Merge branch 'master' of github.com:kubernetes-client/python into release-18b1

This commit is contained in:
Haowei Cai 2021-06-20 19:00:22 -07:00
commit d8fd974e63
49 changed files with 1931 additions and 83 deletions

72
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,72 @@
<!-- Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->
#### What type of PR is this?
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design
Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
#### What this PR does / why we need it:
#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #
#### Special notes for your reviewer:
#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
```
#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:
<!--
This section can be blank if this pull request does not require a release note.
When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.
See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files
Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs
```

44
.github/workflows/e2e-master.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: End to End Tests - master
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Create Kind Cluster
uses: helm/kind-action@v1.1.0
with:
cluster_name: kubernetes-python-e2e-master-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.11.1
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.18.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r test-requirements.txt
- name: Install package
run: python -m pip install -e .
- name: Run End to End tests
run: pytest -vvv -s kubernetes/e2e_test

44
.github/workflows/e2e-release-11.0.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: End to End Tests - release-11.0
on:
push:
branches:
- release-11.0
pull_request:
branches:
- release-11.0
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Create Kind Cluster
uses: helm/kind-action@v1.1.0
with:
cluster_name: kubernetes-python-e2e-release-11.0-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.11.1
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.15.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r test-requirements.txt
- name: Install package
run: python -m pip install -e .
- name: Run End to End tests
run: pytest -vvv -s kubernetes/e2e_test

44
.github/workflows/e2e-release-12.0.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: End to End Tests - release-12.0
on:
push:
branches:
- release-12.0
pull_request:
branches:
- release-12.0
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Create Kind Cluster
uses: helm/kind-action@v1.1.0
with:
cluster_name: kubernetes-python-e2e-release-12.0-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.11.1
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.16.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r test-requirements.txt
- name: Install package
run: python -m pip install -e .
- name: Run End to End tests
run: pytest -vvv -s kubernetes/e2e_test

44
.github/workflows/e2e-release-17.0.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: End to End Tests - release-17.0
on:
push:
branches:
- release-17.0
pull_request:
branches:
- release-17.0
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Create Kind Cluster
uses: helm/kind-action@v1.1.0
with:
cluster_name: kubernetes-python-e2e-release-17.0-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.11.1
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.17.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r test-requirements.txt
- name: Install package
run: python -m pip install -e .
- name: Run End to End tests
run: pytest -vvv -s kubernetes/e2e_test

44
.github/workflows/e2e-release-18.0.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: End to End Tests - release-18.0
on:
push:
branches:
- release-18.0
pull_request:
branches:
- release-18.0
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Create Kind Cluster
uses: helm/kind-action@v1.1.0
with:
cluster_name: kubernetes-python-e2e-release-18.0-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.11.1
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.18.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r test-requirements.txt
- name: Install package
run: python -m pip install -e .
- name: Run End to End tests
run: pytest -vvv -s kubernetes/e2e_test

View File

@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.15.12@sha256:b920920e1eda689d9936dfcf7332701e80be12566999152626b2c9d730397a95
- role: worker
image: kindest/node:v1.15.12@sha256:b920920e1eda689d9936dfcf7332701e80be12566999152626b2c9d730397a95

View File

@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.16.15@sha256:83067ed51bf2a3395b24687094e283a7c7c865ccc12a8b1d7aa673ba0c5e8861
- role: worker
image: kindest/node:v1.16.15@sha256:83067ed51bf2a3395b24687094e283a7c7c865ccc12a8b1d7aa673ba0c5e8861

View File

@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00
- role: worker
image: kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00

View File

@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c
- role: worker
image: kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c

View File

@ -8,14 +8,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.1.4
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies

View File

@ -24,32 +24,20 @@ jobs:
[[ "${TRAVIS_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(([ab]|dev|rc)[0-9]+)?$ ]]
- stage: test
python: 2.7
python: 3.9
env: TOXENV=update-pycodestyle
- python: 3.9
env: TOXENV=coverage,codecov
- python: 3.7
env: TOXENV=docs
- python: 2.7
env: TOXENV=coverage,codecov
- python: 2.7
env: TOXENV=py27
- python: 2.7
env: TOXENV=py27-functional
- python: 3.5
env: TOXENV=py35
- python: 3.5
env: TOXENV=py35-functional
- python: 3.6
env: TOXENV=py36
- python: 3.6
env: TOXENV=py36-functional
- python: 3.7
env: TOXENV=py37
- python: 3.7
env: TOXENV=py37-functional
- python: 3.8
env: TOXENV=py38
- python: 3.8
env: TOXENV=py38-functional
- python: 3.9
env: TOXENV=py39
- stage: deploy
script: skip
deploy:
@ -63,6 +51,38 @@ jobs:
repo: kubernetes-client/python
distributions: sdist bdist_wheel
- stage: test
python: 3.9
env: TOXENV=update-pycodestyle
arch: ppc64le
- python: 3.7
env: TOXENV=docs
arch: ppc64le
- python: 3.6
env: TOXENV=py36
arch: ppc64le
- python: 3.7
env: TOXENV=py37
arch: ppc64le
- python: 3.8
env: TOXENV=py38
- python: 3.9
env: TOXENV=py39
arch: ppc64le
- stage: deploy
script: skip
arch: ppc64le
deploy:
provider: pypi
user: __token__
password:
secure: gY5Rixj7mWHC9XP5qV5DfWGdX4ZVwCEUElnQA2OeIg235I3eMBqRFM4Q/SKwAG2DzgIWNKsXXVQsZHp7BAjWFMFVQloiU7zohuBRToJUim9U1RaqAjUIr4OU7JPtXenAl5zyyBdywvJiG8UZ4wmt1DBYtdpozQvOwDXvOxNTmElKh5mfDhiSsipmFr2198NtIhiRVC+CZliZsi6osUkt+G6yl9CW+SJU3otgzdaS+VBP26HO0kWHMJiDKvQoIl/Q50IqJUWieFhCLh7lSV71VNVEmM4bMcYK8cAv3zMZHo6REKHF7xrF5tzYMXqpmEGt6L798d2H4BISr6BIlYgiYCatjyE9hxih9iBzGs0XaGUUFD8u1iuzOQI76a5dapG/DixQrGD2o9Gn/Qw6Zp9USIuKZSWUn5hSobwxJUKVNy+afpaJNQUb2W9Hj+jMXAnBDodCzo3nu+QF8GN72cmk3uqVyKUVABtI4kNe3qcEx3DyKfoh7aqJrgydeaRwESKuZ41l5CA+vqXSbbNW8z1MYDYgVdwEyRFsLg6aQk5pPsxuiILaaGy13TUndhuC+GuKcW6wCDf6WpUAwwGAF8+sz4hZ1pfSUdE3F8nfDBW3Bv+G9cB/cKkWJ2vOd9httRrvir8qUc/xPP5aW4pacnfNCQ04Iep/k4PCAdYJDtVGhCY=
skip_existing: true
on:
tags: true
repo: kubernetes-client/python
distributions: sdist bdist_wheel
- stage: test
python: 2.7
env: TOXENV=update-pycodestyle

View File

@ -47,6 +47,37 @@ Kubernetes API Version: 1.18.17
To read the full CHANGELOG visit [here](https://raw.githubusercontent.com/kubernetes/kubernetes/master/CHANGELOG/CHANGELOG-1.18.md).
# v17.17.0
Kubernetes API Version: 1.17.17
Changelog since v17.17.0b1:
### Bug or Regression
- Fix watch stream non-chunked response handling ([kubernetes-client/python-base#231](https://github.com/kubernetes-client/python-base/pull/231), [@dhague](https://github.com/dhague))
- Fixed a decoding error for BOOTMARK watch events ([kubernetes-client/python-base#234](https://github.com/kubernetes-client/python-base/pull/234), [@yliaog](https://github.com/yliaog))
### Feature
- Load_kube_config_from_dict() support define custom temp files path ([kubernetes-client/python-base#233](https://github.com/kubernetes-client/python-base/pull/233), [@onecer](https://github.com/onecer))
- The dynamic client now supports customizing http "Accept" header through the `header_params` parameter, which can be used to customizing API server response, e.g. retrieving object metadata only. ([kubernetes-client/python-base#236](https://github.com/kubernetes-client/python-base/pull/236), [@Yashks1994](https://github.com/Yashks1994))
# v17.17.0b1
Kubernetes API Version: 1.17.17
Changelog since v17.14.0a1:
**New Feature:**
- Add Python 3.9 to build [kubernetes-client/python#1311](https://github.com/kubernetes-client/python/pull/1311)
- Enable leaderelection [kubernetes-client/python#1363](https://github.com/kubernetes-client/python/pull/1363)
**API Change:**
- Add allowWatchBookmarks, resoureVersionMatch parameters to custom objects. [kubernetes-client/gen#180](https://github.com/kubernetes-client/gen/pull/180)
**Bug Fix:**
- fix: load cache error when CacheDecoder object is not callable [kubernetes-client/python-base#226](https://github.com/kubernetes-client/python-base/pull/226)
- raise exception when an empty config file is passed to load_kube_config [kubernetes-client/python-base#223](https://github.com/kubernetes-client/python-base/pull/223)
- Fix bug with Watch and 410 retries [kubernetes-client/python-base#227](https://github.com/kubernetes-client/python-base/pull/227)
# v17.14.0a1

View File

@ -140,7 +140,7 @@ this step and go back to the master branch if there are any API changes.
## Make distribution packages
First make sure you are using a clean version of python. Use virtualenv and
pyenv packages. Make sure you are using python 2.7.12. I would normally do this
pyenv packages. Make sure you are using python 3.9.1. I would normally do this
on a clean machine:
(install [pyenv](https://github.com/yyuu/pyenv#installation))
@ -149,11 +149,11 @@ on a clean machine:
```bash
git clean -xdf
pyenv install -s 2.7.12
pyenv global 2.7.12
pyenv install -s 3.9.1
pyenv global 3.9.1
virtualenv .release
source .release/bin/activate
python --version # Make sure you get Python 2.7.12
python --version # Make sure you get Python 3.9.1
pip install twine
```

View File

@ -23,7 +23,7 @@ git submodule update --init
If you changed [kubernetes-client/python-base](https://github.com/kubernetes-client/python-base) and want to pull your changes into this repo run this command:
```bash
git submodule update --remote
scripts/update-submodule.sh
```
Once updated, you should create a new PR to commit changes to the repository.
After the script finishes, please create a commit "generated python-base update" and send a PR to this repository.

View File

@ -0,0 +1,150 @@
# Copyright 2021 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 Resource (CR), 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/
Apply the following yaml manifest to create a cluster-scoped CustomResourceDefinition (CRD)
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
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
scope: Cluster
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()
# definition of custom resource
test_resource = {
"apiVersion": "stable.example.com/v1",
"kind": "CronTab",
"metadata": {"name": "test-crontab"},
"spec": {"cronSpec": "* * * * */5", "image": "my-awesome-cron-image"},
}
# patch to update the `spec.cronSpec` field
cronspec_patch = {
"spec": {"cronSpec": "* * * * */15", "image": "my-awesome-cron-image"}
}
# patch to add the `metadata.labels` field
metadata_label_patch = {
"metadata": {
"labels": {
"foo": "bar",
}
}
}
# create a cluster scoped resource
created_resource = api.create_cluster_custom_object(
group="stable.example.com",
version="v1",
plural="crontabs",
body=test_resource,
)
print("[INFO] Custom resource `test-crontab` created!\n")
# get the cluster scoped resource
resource = api.get_cluster_custom_object(
group="stable.example.com",
version="v1",
name="test-crontab",
plural="crontabs",
)
print("%s\t\t%s" % ("NAME", "CRON-SPEC"))
print(
"%s\t%s\n" %
(resource["metadata"]["name"],
resource["spec"]["cronSpec"]))
# patch the `spec.cronSpec` field of the custom resource
patched_resource = api.patch_cluster_custom_object(
group="stable.example.com",
version="v1",
plural="crontabs",
name="test-crontab",
body=cronspec_patch,
)
print("[INFO] Custom resource `test-crontab` patched to update the cronSpec schedule!\n")
print("%s\t\t%s" % ("NAME", "PATCHED-CRON-SPEC"))
print(
"%s\t%s\n" %
(patched_resource["metadata"]["name"],
patched_resource["spec"]["cronSpec"]))
# patch the `metadata.labels` field of the custom resource
patched_resource = api.patch_cluster_custom_object(
group="stable.example.com",
version="v1",
plural="crontabs",
name="test-crontab",
body=metadata_label_patch,
)
print("[INFO] Custom resource `test-crontab` patched to apply new metadata labels!\n")
print("%s\t\t%s" % ("NAME", "PATCHED_LABELS"))
print(
"%s\t%s\n" %
(patched_resource["metadata"]["name"],
patched_resource["metadata"]["labels"]))
# delete the custom resource "test-crontab"
api.delete_cluster_custom_object(
group="stable.example.com",
version="v1",
name="test-crontab",
plural="crontabs",
body=client.V1DeleteOptions(),
)
print("[INFO] Custom resource `test-crontab` deleted!")
if __name__ == "__main__":
main()

View File

@ -12,6 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Creates a deployment using AppsV1Api from file nginx-deployment.yaml.
"""
from os import path
import yaml

View File

@ -13,9 +13,16 @@
# limitations under the License.
"""
Creates, updates, and deletes a deployment using AppsV1Api.
The example covers the following:
- Creation of a deployment using AppsV1Api
- update/patch to perform rolling restart on the deployment
- deletetion of the deployment
"""
import datetime
import pytz
from kubernetes import client, config
DEPLOYMENT_NAME = "nginx-deployment"
@ -29,56 +36,110 @@ def create_deployment_object():
ports=[client.V1ContainerPort(container_port=80)],
resources=client.V1ResourceRequirements(
requests={"cpu": "100m", "memory": "200Mi"},
limits={"cpu": "500m", "memory": "500Mi"}
)
limits={"cpu": "500m", "memory": "500Mi"},
),
)
# Create and configurate a spec section
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={"app": "nginx"}),
spec=client.V1PodSpec(containers=[container]))
spec=client.V1PodSpec(containers=[container]),
)
# Create the specification of deployment
spec = client.V1DeploymentSpec(
replicas=3,
template=template,
selector={'matchLabels': {'app': 'nginx'}})
replicas=3, template=template, selector={
"matchLabels":
{"app": "nginx"}})
# Instantiate the deployment object
deployment = client.V1Deployment(
api_version="apps/v1",
kind="Deployment",
metadata=client.V1ObjectMeta(name=DEPLOYMENT_NAME),
spec=spec)
spec=spec,
)
return deployment
def create_deployment(api_instance, deployment):
def create_deployment(api, deployment):
# Create deployement
api_response = api_instance.create_namespaced_deployment(
body=deployment,
namespace="default")
print("Deployment created. status='%s'" % str(api_response.status))
resp = api.create_namespaced_deployment(
body=deployment, namespace="default"
)
print("\n[INFO] deployment `nginx-deployment` created.\n")
print("%s\t%s\t\t\t%s\t%s" % ("NAMESPACE", "NAME", "REVISION", "IMAGE"))
print(
"%s\t\t%s\t%s\t\t%s\n"
% (
resp.metadata.namespace,
resp.metadata.name,
resp.metadata.generation,
resp.spec.template.spec.containers[0].image,
)
)
def update_deployment(api_instance, deployment):
def update_deployment(api, deployment):
# Update container image
deployment.spec.template.spec.containers[0].image = "nginx:1.16.0"
# Update the deployment
api_response = api_instance.patch_namespaced_deployment(
name=DEPLOYMENT_NAME,
namespace="default",
body=deployment)
print("Deployment updated. status='%s'" % str(api_response.status))
# patch the deployment
resp = api.patch_namespaced_deployment(
name=DEPLOYMENT_NAME, namespace="default", body=deployment
)
print("\n[INFO] deployment's container image updated.\n")
print("%s\t%s\t\t\t%s\t%s" % ("NAMESPACE", "NAME", "REVISION", "IMAGE"))
print(
"%s\t\t%s\t%s\t\t%s\n"
% (
resp.metadata.namespace,
resp.metadata.name,
resp.metadata.generation,
resp.spec.template.spec.containers[0].image,
)
)
def delete_deployment(api_instance):
def restart_deployment(api, deployment):
# update `spec.template.metadata` section
# to add `kubectl.kubernetes.io/restartedAt` annotation
deployment.spec.template.metadata.annotations = {
"kubectl.kubernetes.io/restartedAt": datetime.datetime.utcnow()
.replace(tzinfo=pytz.UTC)
.isoformat()
}
# patch the deployment
resp = api.patch_namespaced_deployment(
name=DEPLOYMENT_NAME, namespace="default", body=deployment
)
print("\n[INFO] deployment `nginx-deployment` restarted.\n")
print("%s\t\t\t%s\t%s" % ("NAME", "REVISION", "RESTARTED-AT"))
print(
"%s\t%s\t\t%s\n"
% (
resp.metadata.name,
resp.metadata.generation,
resp.spec.template.metadata.annotations,
)
)
def delete_deployment(api):
# Delete deployment
api_response = api_instance.delete_namespaced_deployment(
resp = api.delete_namespaced_deployment(
name=DEPLOYMENT_NAME,
namespace="default",
body=client.V1DeleteOptions(
propagation_policy='Foreground',
grace_period_seconds=5))
print("Deployment deleted. status='%s'" % str(api_response.status))
propagation_policy="Foreground", grace_period_seconds=5
),
)
print("\n[INFO] deployment `nginx-deployment` deleted.")
def main():
@ -101,8 +162,10 @@ def main():
update_deployment(apps_v1, deployment)
restart_deployment(apps_v1, deployment)
delete_deployment(apps_v1)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -0,0 +1,43 @@
# Copyright 2021 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.
"""
This example demonstrates how to pass the custom header in the cluster.
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the node api
api = client.resources.get(api_version="v1", kind="Node")
# Creating a custom header
params = {'header_params': {'Accept': 'application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io'}}
resp = api.get(**params)
# Printing the kind and apiVersion after passing new header params.
print("%s\t\t\t%s" %("VERSION", "KIND"))
print("%s\t\t%s" %(resp.apiVersion, resp.kind))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,213 @@
# Copyright 2021 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.
"""
This example demonstrates the following:
- Creation of a custom resource definition (CRD) using dynamic-client
- Creation of cluster scoped custom resources (CR) using the above created CRD
- List, patch (update), delete the custom resources
- Delete the custom resource defintion (CRD)
"""
from kubernetes import config, dynamic
from kubernetes.dynamic.exceptions import ResourceNotFoundError
from kubernetes.client import api_client
import time
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the custom resource definition (CRD) api
crd_api = client.resources.get(
api_version="apiextensions.k8s.io/v1", kind="CustomResourceDefinition"
)
# Creating a Namespaced CRD named "ingressroutes.apps.example.com"
name = "ingressroutes.apps.example.com"
crd_manifest = {
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {
"name": name,
},
"spec": {
"group": "apps.example.com",
"versions": [
{
"name": "v1",
"schema": {
"openAPIV3Schema": {
"properties": {
"spec": {
"properties": {
"strategy": {"type": "string"},
"virtualhost": {
"properties": {
"fqdn": {"type": "string"},
"tls": {
"properties": {
"secretName": {"type": "string"}
},
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
},
"type": "object",
}
},
"served": True,
"storage": True,
}
],
"scope": "Cluster",
"names": {
"plural": "ingressroutes",
"listKind": "IngressRouteList",
"singular": "ingressroute",
"kind": "IngressRoute",
"shortNames": ["ir"],
},
},
}
crd_creation_response = crd_api.create(crd_manifest)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` created\n"
)
print("%s\t\t%s" % ("SCOPE", "NAME"))
print(
"%s\t\t%s\n"
% (crd_creation_response.spec.scope, crd_creation_response.metadata.name)
)
# Fetching the "ingressroutes" CRD api
try:
ingressroute_api = client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)
except ResourceNotFoundError:
# Need to wait a sec for the discovery layer to get updated
time.sleep(2)
ingressroute_api = client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)
# Creating a custom resource (CR) `ingress-route-*`, using the above CRD `ingressroutes.apps.example.com`
ingressroute_manifest_first = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-first",
},
"spec": {
"virtualhost": {
"fqdn": "www.google.com",
"tls": {"secretName": "google-tls"},
},
"strategy": "RoundRobin",
},
}
ingressroute_manifest_second = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-second",
},
"spec": {
"virtualhost": {
"fqdn": "www.yahoo.com",
"tls": {"secretName": "yahoo-tls"},
},
"strategy": "RoundRobin",
},
}
ingressroute_api.create(body=ingressroute_manifest_first)
ingressroute_api.create(body=ingressroute_manifest_second)
print("\n[INFO] custom resources `ingress-route-*` created\n")
# Listing the `ingress-route-*` custom resources
ingress_routes_list = ingressroute_api.get()
print("%s\t\t\t%s\t\t%s\t\t\t\t%s" % ("NAME", "FQDN", "TLS", "STRATEGY"))
for item in ingress_routes_list.items:
print(
"%s\t%s\t%s\t%s"
% (
item.metadata.name,
item.spec.virtualhost.fqdn,
item.spec.virtualhost.tls,
item.spec.strategy,
)
)
# Patching the ingressroutes custom resources
ingressroute_manifest_first["spec"]["strategy"] = "Random"
ingressroute_manifest_second["spec"]["strategy"] = "WeightedLeastRequest"
patch_ingressroute_first = ingressroute_api.patch(
body=ingressroute_manifest_first, content_type="application/merge-patch+json"
)
patch_ingressroute_second = ingressroute_api.patch(
body=ingressroute_manifest_second, content_type="application/merge-patch+json"
)
print(
"\n[INFO] custom resources `ingress-route-*` patched to update the strategy\n"
)
patched_ingress_routes_list = ingressroute_api.get()
print("%s\t\t\t%s\t\t%s\t\t\t\t%s" % ("NAME", "FQDN", "TLS", "STRATEGY"))
for item in patched_ingress_routes_list.items:
print(
"%s\t%s\t%s\t%s"
% (
item.metadata.name,
item.spec.virtualhost.fqdn,
item.spec.virtualhost.tls,
item.spec.strategy,
)
)
# Deleting the ingressroutes custom resources
delete_ingressroute_first = ingressroute_api.delete(name="ingress-route-first")
delete_ingressroute_second = ingressroute_api.delete(name="ingress-route-second")
print("\n[INFO] custom resources `ingress-route-*` deleted")
# Deleting the ingressroutes.apps.example.com custom resource definition
crd_api.delete(name=name)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` deleted"
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,85 @@
# Copyright 2021 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.
"""
This example demonstrates the following:
- Creation of a k8s configmap using dynamic-client
- List, patch(update), delete the configmap
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the configmap api
api = client.resources.get(api_version="v1", kind="ConfigMap")
configmap_name = "test-configmap"
configmap_manifest = {
"kind": "ConfigMap",
"apiVersion": "v1",
"metadata": {
"name": configmap_name,
"labels": {
"foo": "bar",
},
},
"data": {
"config.json": '{"command":"/usr/bin/mysqld_safe"}',
"frontend.cnf": "[mysqld]\nbind-address = 10.0.0.3\n",
},
}
# Creating configmap `test-configmap` in the `default` namespace
configmap = api.create(body=configmap_manifest, namespace="default")
print("\n[INFO] configmap `test-configmap` created\n")
# Listing the configmaps in the `default` namespace
configmap_list = api.get(
name=configmap_name, namespace="default", label_selector="foo=bar"
)
print("NAME:\n%s\n" % (configmap_list.metadata.name))
print("DATA:\n%s\n" % (configmap_list.data))
# Updating the configmap's data, `config.json`
configmap_manifest["data"]["config.json"] = "{}"
configmap_patched = api.patch(
name=configmap_name, namespace="default", body=configmap_manifest
)
print("\n[INFO] configmap `test-configmap` patched\n")
print("NAME:\n%s\n" % (configmap_patched.metadata.name))
print("DATA:\n%s\n" % (configmap_patched.data))
# Deleting configmap `test-configmap` from the `default` namespace
configmap_deleted = api.delete(name=configmap_name, body={}, namespace="default")
print("\n[INFO] configmap `test-configmap` deleted\n")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,120 @@
# Copyright 2021 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.
"""
This example demonstrates the following:
- Creation of a k8s deployment using dynamic-client
- Rolling restart of the deployment (demonstrate patch/update action)
- Listing & deletion of the deployment
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
import datetime
import pytz
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the deployment api
api = client.resources.get(api_version="apps/v1", kind="Deployment")
name = "nginx-deployment"
deployment_manifest = {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {"labels": {"app": "nginx"}, "name": name},
"spec": {
"replicas": 3,
"selector": {"matchLabels": {"app": "nginx"}},
"template": {
"metadata": {"labels": {"app": "nginx"}},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:1.14.2",
"ports": [{"containerPort": 80}],
}
]
},
},
},
}
# Creating deployment `nginx-deployment` in the `default` namespace
deployment = api.create(body=deployment_manifest, namespace="default")
print("\n[INFO] deployment `nginx-deployment` created\n")
# Listing deployment `nginx-deployment` in the `default` namespace
deployment_created = api.get(name=name, namespace="default")
print("%s\t%s\t\t\t%s\t%s" % ("NAMESPACE", "NAME", "REVISION", "RESTARTED-AT"))
print(
"%s\t\t%s\t%s\t\t%s\n"
% (
deployment_created.metadata.namespace,
deployment_created.metadata.name,
deployment_created.metadata.annotations,
deployment_created.spec.template.metadata.annotations,
)
)
# Patching the `spec.template.metadata` section to add `kubectl.kubernetes.io/restartedAt` annotation
# In order to perform a rolling restart on the deployment `nginx-deployment`
deployment_manifest["spec"]["template"]["metadata"] = {
"annotations": {
"kubectl.kubernetes.io/restartedAt": datetime.datetime.utcnow()
.replace(tzinfo=pytz.UTC)
.isoformat()
}
}
deployment_patched = api.patch(
body=deployment_manifest, name=name, namespace="default"
)
print("\n[INFO] deployment `nginx-deployment` restarted\n")
print(
"%s\t%s\t\t\t%s\t\t\t\t\t\t%s"
% ("NAMESPACE", "NAME", "REVISION", "RESTARTED-AT")
)
print(
"%s\t\t%s\t%s\t\t%s\n"
% (
deployment_patched.metadata.namespace,
deployment_patched.metadata.name,
deployment_patched.metadata.annotations,
deployment_patched.spec.template.metadata.annotations,
)
)
# Deleting deployment `nginx-deployment` from the `default` namespace
deployment_deleted = api.delete(name=name, body={}, namespace="default")
print("\n[INFO] deployment `nginx-deployment` deleted\n")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,250 @@
# Copyright 2021 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.
"""
This example demonstrates the following:
- Creation of a custom resource definition (CRD) using dynamic-client
- Creation of namespaced custom resources (CR) using the above CRD
- List, patch (update), delete the custom resources
- Delete the custom resource defintion (CRD)
"""
from kubernetes import config, dynamic
from kubernetes import client as k8s_client
from kubernetes.dynamic.exceptions import ResourceNotFoundError
from kubernetes.client import api_client
import time
def list_ingressroute_for_all_namespaces(group, version, plural):
custom_object_api = k8s_client.CustomObjectsApi()
list_of_ingress_routes = custom_object_api.list_cluster_custom_object(
group, version, plural
)
print(
"%s\t\t\t%s\t\t\t%s\t\t%s\t\t\t\t%s"
% ("NAME", "NAMESPACE", "FQDN", "TLS", "STRATEGY")
)
for item in list_of_ingress_routes["items"]:
print(
"%s\t%s\t\t%s\t%s\t%s"
% (
item["metadata"]["name"],
item["metadata"]["namespace"],
item["spec"]["virtualhost"]["fqdn"],
item["spec"]["virtualhost"]["tls"],
item["spec"]["strategy"]
)
)
def create_namespace(namespace_api, name):
namespace_manifest = {
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {"name": name, "resourceversion": "v1"},
}
namespace_api.create(body=namespace_manifest)
def delete_namespace(namespace_api, name):
namespace_api.delete(name=name)
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the custom resource definition (CRD) api
crd_api = client.resources.get(
api_version="apiextensions.k8s.io/v1", kind="CustomResourceDefinition"
)
namespace_api = client.resources.get(api_version="v1", kind="Namespace")
# Creating a Namespaced CRD named "ingressroutes.apps.example.com"
name = "ingressroutes.apps.example.com"
crd_manifest = {
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {"name": name, "namespace": "default"},
"spec": {
"group": "apps.example.com",
"versions": [
{
"name": "v1",
"schema": {
"openAPIV3Schema": {
"properties": {
"spec": {
"properties": {
"strategy": {"type": "string"},
"virtualhost": {
"properties": {
"fqdn": {"type": "string"},
"tls": {
"properties": {
"secretName": {"type": "string"}
},
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
},
"type": "object",
}
},
"served": True,
"storage": True,
}
],
"scope": "Namespaced",
"names": {
"plural": "ingressroutes",
"listKind": "IngressRouteList",
"singular": "ingressroute",
"kind": "IngressRoute",
"shortNames": ["ir"],
},
},
}
crd_creation_respone = crd_api.create(crd_manifest)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` created\n"
)
print("%s\t\t%s" % ("SCOPE", "NAME"))
print(
"%s\t%s\n"
% (crd_creation_respone.spec.scope, crd_creation_respone.metadata.name)
)
# Fetching the "ingressroutes" CRD api
try:
ingressroute_api = client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)
except ResourceNotFoundError:
# Need to wait a sec for the discovery layer to get updated
time.sleep(2)
ingressroute_api = client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)
# Creating a custom resource (CR) `ingress-route-*`, using the above CRD `ingressroutes.apps.example.com`
namespace_first = "test-namespace-first"
namespace_second = "test-namespace-second"
create_namespace(namespace_api, namespace_first)
create_namespace(namespace_api, namespace_second)
ingressroute_manifest_first = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-first",
"namespace": namespace_first,
},
"spec": {
"virtualhost": {
"fqdn": "www.google.com",
"tls": {"secretName": "google-tls"},
},
"strategy": "RoundRobin",
},
}
ingressroute_manifest_second = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-second",
"namespace": namespace_second,
},
"spec": {
"virtualhost": {
"fqdn": "www.yahoo.com",
"tls": {"secretName": "yahoo-tls"},
},
"strategy": "RoundRobin",
},
}
ingressroute_api.create(body=ingressroute_manifest_first, namespace=namespace_first)
ingressroute_api.create(body=ingressroute_manifest_second, namespace=namespace_second)
print("\n[INFO] custom resources `ingress-route-*` created\n")
# Listing the `ingress-route-*` custom resources
list_ingressroute_for_all_namespaces(
group="apps.example.com", version="v1", plural="ingressroutes"
)
# Patching the ingressroutes custom resources
ingressroute_manifest_first["spec"]["strategy"] = "Random"
ingressroute_manifest_second["spec"]["strategy"] = "WeightedLeastRequest"
patch_ingressroute_first = ingressroute_api.patch(
body=ingressroute_manifest_first, content_type="application/merge-patch+json"
)
patch_ingressroute_second = ingressroute_api.patch(
body=ingressroute_manifest_second, content_type="application/merge-patch+json"
)
print(
"\n[INFO] custom resources `ingress-route-*` patched to update the strategy\n"
)
list_ingressroute_for_all_namespaces(
group="apps.example.com", version="v1", plural="ingressroutes"
)
# Deleting the ingressroutes custom resources
delete_ingressroute_first = ingressroute_api.delete(
name="ingress-route-first", namespace=namespace_first
)
delete_ingressroute_second = ingressroute_api.delete(
name="ingress-route-second", namespace=namespace_second
)
print("\n[INFO] custom resources `ingress-route-*` deleted")
# Deleting the namespaces
delete_namespace(namespace_api, namespace_first)
time.sleep(4)
delete_namespace(namespace_api, namespace_second)
time.sleep(4)
print("\n[INFO] test namespaces deleted")
# Deleting the ingressroutes.apps.example.com custom resource definition
crd_api.delete(name=name)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` deleted"
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,49 @@
# Copyright 2021 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.
"""
This example demonstrates how to list cluster nodes using dynamic client.
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the node api
api = client.resources.get(api_version="v1", kind="Node")
# Listing cluster nodes
print("%s\t\t%s\t\t%s" % ("NAME", "STATUS", "VERSION"))
for item in api.get().items:
node = api.get(name=item.metadata.name)
print(
"%s\t%s\t\t%s\n"
% (
node.metadata.name,
node.status.conditions[3]["type"],
node.status.nodeInfo.kubeProxyVersion,
)
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,84 @@
# Copyright 2021 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.
"""
This example demonstrates the creation, listing & deletion of a namespaced replication controller using dynamic-client.
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the replication controller api
api = client.resources.get(api_version="v1", kind="ReplicationController")
name = "frontend-replication-controller"
replication_controller_manifest = {
"apiVersion": "v1",
"kind": "ReplicationController",
"metadata": {"labels": {"name": name}, "name": name},
"spec": {
"replicas": 2,
"selector": {"name": name},
"template": {
"metadata": {"labels": {"name": name}},
"spec": {
"containers": [
{
"image": "nginx",
"name": "nginx",
"ports": [{"containerPort": 80, "protocol": "TCP"}],
}
]
},
},
},
}
# Creating replication-controller `frontend-replication-controller` in the `default` namespace
replication_controller = api.create(
body=replication_controller_manifest, namespace="default"
)
print("\n[INFO] replication-controller `frontend-replication-controller` created\n")
# Listing replication-controllers in the `default` namespace
replication_controller_created = api.get(name=name, namespace="default")
print("%s\t%s\t\t\t\t\t%s" % ("NAMESPACE", "NAME", "REPLICAS"))
print(
"%s\t\t%s\t\t%s\n"
% (
replication_controller_created.metadata.namespace,
replication_controller_created.metadata.name,
replication_controller_created.spec.replicas,
)
)
# Deleting replication-controller `frontend-service` from the `default` namespace
replication_controller_deleted = api.delete(name=name, body={}, namespace="default")
print("[INFO] replication-controller `frontend-replication-controller` deleted\n")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,89 @@
# Copyright 2021 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.
"""
This example demonstrates the following:
- Creation of a k8s service using dynamic-client
- List, patch(update), delete the service
"""
from kubernetes import config, dynamic
from kubernetes.client import api_client
def main():
# Creating a dynamic client
client = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
# fetching the service api
api = client.resources.get(api_version="v1", kind="Service")
name = "frontend-service"
service_manifest = {
"apiVersion": "v1",
"kind": "Service",
"metadata": {"labels": {"name": name}, "name": name, "resourceversion": "v1"},
"spec": {
"ports": [
{"name": "port", "port": 80, "protocol": "TCP", "targetPort": 80}
],
"selector": {"name": name},
},
}
# Creating service `frontend-service` in the `default` namespace
service = api.create(body=service_manifest, namespace="default")
print("\n[INFO] service `frontend-service` created\n")
# Listing service `frontend-service` in the `default` namespace
service_created = api.get(name=name, namespace="default")
print("%s\t%s" % ("NAMESPACE", "NAME"))
print(
"%s\t\t%s\n"
% (service_created.metadata.namespace, service_created.metadata.name)
)
# Patching the `spec` section of the `frontend-service`
service_manifest["spec"]["ports"] = [
{"name": "new", "port": 8080, "protocol": "TCP", "targetPort": 8080}
]
service_patched = api.patch(body=service_manifest, name=name, namespace="default")
print("\n[INFO] service `frontend-service` patched\n")
print("%s\t%s\t\t\t%s" % ("NAMESPACE", "NAME", "PORTS"))
print(
"%s\t\t%s\t%s\n"
% (
service_patched.metadata.namespace,
service_patched.metadata.name,
service_patched.spec.ports,
)
)
# Deleting service `frontend-service` from the `default` namespace
service_deleted = api.delete(name=name, body={}, namespace="default")
print("\n[INFO] service `frontend-service` deleted\n")
if __name__ == "__main__":
main()

View File

@ -17,6 +17,7 @@ Creates, updates, and deletes a job object.
"""
from os import path
from time import sleep
import yaml
@ -54,6 +55,20 @@ def create_job(api_instance, job):
body=job,
namespace="default")
print("Job created. status='%s'" % str(api_response.status))
get_job_status(api_instance)
def get_job_status(api_instance):
job_completed = False
while not job_completed:
api_response = api_instance.read_namespaced_job_status(
name=JOB_NAME,
namespace="default")
if api_response.status.succeeded is not None or \
api_response.status.failed is not None:
job_completed = True
sleep(1)
print("Job status='%s'" % str(api_response.status))
def update_job(api_instance, job):

View File

@ -18,9 +18,10 @@ 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 pick import pick # install pick using `pip install pick`
from kubernetes import client, config
from kubernetes.client import configuration
from pick import pick # install pick using `pip install pick`
def main():

View File

@ -17,7 +17,7 @@ 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:
The following yaml manifest has to be applied first for namespaced scoped CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
@ -29,6 +29,19 @@ spec:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
scope: Namespaced
names:
plural: crontabs
@ -59,6 +72,11 @@ def main():
}
}
# patch to update the `spec.cronSpec` field
patch_body = {
"spec": {"cronSpec": "* * * * */10", "image": "my-awesome-cron-image"}
}
# create the resource
api.create_namespaced_custom_object(
group="stable.example.com",
@ -80,6 +98,18 @@ def main():
print("Resource details:")
pprint(resource)
# patch the namespaced custom object to update the `spec.cronSpec` field
patch_resource = api.patch_namespaced_custom_object(
group="stable.example.com",
version="v1",
name="my-new-cron-object",
namespace="default",
plural="crontabs",
body=patch_body,
)
print("Resource details:")
pprint(patch_resource)
# delete it
api.delete_namespaced_custom_object(
group="stable.example.com",

View File

@ -13,13 +13,14 @@
# 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".
This example demonstrates the following:
- Get a list of all the cluster nodes
- Iterate through each node list item
- Add or overwirite label "foo" with the value "bar"
- Remove the label "baz"
- Return the list of node with updated labels
"""
from pprint import pprint
from kubernetes import client, config
@ -36,9 +37,14 @@ def main():
}
}
api_response = api_instance.patch_node("minikube", body)
# Listing the cluster nodes
node_list = api_instance.list_node()
pprint(api_response)
print("%s\t\t%s" % ("NAME", "LABELS"))
# Patching the node labels
for node in node_list.items:
api_response = api_instance.patch_node(node.metadata.name, body)
print("%s\t%s" % (node.metadata.name, node.metadata.labels))
if __name__ == '__main__':

View File

@ -18,9 +18,10 @@ 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 pick import pick # install pick using `pip install pick`
from kubernetes import client, config
from kubernetes.client import configuration
from pick import pick # install pick using `pip install pick`
def main():

View File

@ -19,9 +19,10 @@ context includes a cluster, a user, and a namespace.
Please install the pick library before running this example.
"""
from pick import pick # install pick using `pip install pick`
from kubernetes import client, config
from kubernetes.client import configuration
from pick import pick # install pick using `pip install pick`
def main():

View File

@ -12,15 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# This example demonstrate communication with a remote Kube cluster from a
# server outside of the cluster without kube client installed on it.
# The communication is secured with the use of Bearer token.
"""
This example demonstrates the communication between a remote cluster and a
server outside the cluster without kube client installed on it.
The communication is secured with the use of Bearer token.
"""
from kubernetes import client, config
def main():
# Define the barer token we are going to use to authenticate.
# Define the bearer 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 = "<token>"

View File

@ -4,7 +4,7 @@ No description provided (generated by Openapi Generator https://github.com/opena
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: release-1.18
- Package version: 18.17.0a1
- Package version: 18.0.0-snapshot
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
## Requirements.

View File

@ -14,7 +14,7 @@
__project__ = 'kubernetes'
# The version is auto-updated. Please do not edit.
__version__ = "18.17.0a1"
__version__ = "18.0.0-snapshot"
import kubernetes.client
import kubernetes.config

View File

@ -14,7 +14,7 @@
from __future__ import absolute_import
__version__ = "18.17.0a1"
__version__ = "18.0.0-snapshot"
# import apis into sdk package
from kubernetes.client.api.admissionregistration_api import AdmissionregistrationApi

View File

@ -78,7 +78,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/18.17.0a1/python'
self.user_agent = 'OpenAPI-Generator/18.0.0-snapshot/python'
self.client_side_validation = configuration.client_side_validation
def __enter__(self):

View File

@ -347,7 +347,7 @@ class Configuration(object):
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: release-1.18\n"\
"SDK Package Version: 18.17.0a1".\
"SDK Package Version: 18.0.0-snapshot".\
format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self):

View File

@ -19,15 +19,22 @@ import socket
import time
import unittest
import uuid
import six
from kubernetes.client import api_client
from kubernetes.client.api import core_v1_api
from kubernetes.e2e_test import base
from kubernetes.stream import stream, portforward
from kubernetes.stream.ws_client import ERROR_CHANNEL
from kubernetes.client.rest import ApiException
import six.moves.urllib.request as urllib_request
if six.PY3:
from http import HTTPStatus
else:
import httplib
def short_uuid():
id = str(uuid.uuid4())
return id[-12:]
@ -65,6 +72,27 @@ class TestClient(unittest.TestCase):
name = 'busybox-test-' + short_uuid()
pod_manifest = manifest_with_command(name, "while true;do date;sleep 5; done")
# wait for the default service account to be created
timeout = time.time() + 30
while True:
if time.time() > timeout:
print('timeout waiting for default service account creation')
break
try:
resp = api.read_namespaced_service_account(name='default',
namespace='default')
except ApiException as e:
if (six.PY3 and e.status != HTTPStatus.NOT_FOUND) or (
six.PY3 is False and e.status != httplib.NOT_FOUND):
print('error: %s' % e)
self.fail(msg="unexpected error getting default service account")
print('default service not found yet: %s' % e)
time.sleep(1)
continue
self.assertEqual('default', resp.metadata.name)
break
resp = api.create_namespaced_pod(body=pod_manifest,
namespace='default')
self.assertEqual(name, resp.metadata.name)
@ -130,6 +158,28 @@ class TestClient(unittest.TestCase):
name = 'busybox-test-' + short_uuid()
pod_manifest = manifest_with_command(name, "while true;do date;sleep 5; done")
# wait for the default service account to be created
timeout = time.time() + 30
while True:
if time.time() > timeout:
print('timeout waiting for default service account creation')
break
try:
resp = api.read_namespaced_service_account(name='default',
namespace='default')
except ApiException as e:
if (six.PY3 and e.status != HTTPStatus.NOT_FOUND) or (
six.PY3 is False and e.status != httplib.NOT_FOUND):
print('error: %s' % e)
self.fail(msg="unexpected error getting default service account")
print('default service not found yet: %s' % e)
time.sleep(1)
continue
self.assertEqual('default', resp.metadata.name)
break
resp = api.create_namespaced_pod(body=pod_manifest,
namespace='default')
self.assertEqual(name, resp.metadata.name)
@ -164,6 +214,10 @@ class TestClient(unittest.TestCase):
resp = api.delete_namespaced_pod(name=name, body={},
namespace='default')
# Skipping this test as this flakes a lot
# See: https://github.com/kubernetes-client/python/issues/1300
# Re-enable the test once the flakiness is investigated
@unittest.skip("skipping due to extreme flakiness")
def test_portforward_raw(self):
client = api_client.ApiClient(configuration=self.config)
api = core_v1_api.CoreV1Api(client)

View File

@ -2,7 +2,7 @@ certifi>=14.05.14 # MPL
six>=1.9.0 # MIT
python-dateutil>=2.5.3 # BSD
setuptools>=21.0.0 # PSF/ZPL
pyyaml>=3.12 # MIT
pyyaml>=5.4.1 # MIT
google-auth>=1.0.1 # Apache-2.0
ipaddress>=1.0.17;python_version=="2.7" # PSF
websocket-client>=0.32.0,!=0.40.0,!=0.41.*,!=0.42.* # LGPLv2+

View File

@ -38,7 +38,7 @@ fi
# UPDATE: The commit being cherry-picked is updated since the the client generated in 1adaaecd0879d7315f48259ad8d6cbd66b835385
# differs from the initial hotfix
# Ref: https://github.com/kubernetes-client/python/pull/995/commits/9959273625b999ae9a8f0679c4def2ee7d699ede
git cherry-pick -n a138dcbb7a9da972402a847ce982b027e0224e60
git cherry-pick -n 9959273625b999ae9a8f0679c4def2ee7d699ede
if [ $? -eq 0 ]
then
echo Succesfully patched changes for custom client behavior
@ -51,7 +51,7 @@ fi
# Patching commits for enabling from kubernetes import apis
# UPDATE: The commit being cherry-picked is updated to include both the commits as one
# Ref: https://github.com/kubernetes-client/python/blob/0976d59d6ff206f2f428cabc7a6b7b1144843b2a/kubernetes/client/apis/__init__.py
git cherry-pick -n 228a29a982aee922831c3af4fef66a7846ce4bb8
git cherry-pick -n 56ab983036bcb5c78eee91483c1e610da69216d1
if [ $? -eq 0 ]
then
echo Succesfully patched changes for enabling from kubernetes import apis

View File

@ -18,7 +18,7 @@ import sys
KUBERNETES_BRANCH = "release-1.18"
# client version for packaging and releasing.
CLIENT_VERSION = "18.17.0a1"
CLIENT_VERSION = "18.0.0-snapshot"
# Name of the release package
PACKAGE_NAME = "kubernetes"

View File

@ -21,6 +21,9 @@ set -o errexit
set -o nounset
set -o pipefail
# The openapi-generator version used by this client
export OPENAPI_GENERATOR_COMMIT="v4.3.0"
SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")
CLIENT_ROOT="${SCRIPT_ROOT}/../kubernetes"
CLIENT_VERSION=$(python "${SCRIPT_ROOT}/constants.py" CLIENT_VERSION)
@ -31,11 +34,14 @@ pushd "${SCRIPT_ROOT}" > /dev/null
SCRIPT_ROOT=`pwd`
popd > /dev/null
source ${SCRIPT_ROOT}/util/common.sh
util::common::check_sed
pushd "${CLIENT_ROOT}" > /dev/null
CLIENT_ROOT=`pwd`
popd > /dev/null
TEMP_FOLDER=$(mktemp -d)
TEMP_FOLDER=$(mktemp -d)
trap "rm -rf ${TEMP_FOLDER}" EXIT SIGINT
SETTING_FILE="${TEMP_FOLDER}/settings"

View File

@ -67,7 +67,7 @@ done
echo "--- applying isort"
for SOURCE in $SOURCES; do
isort -y $SOURCE
isort $SOURCE
done
echo "--- check pycodestyle (all need to be fixed manually)"

69
scripts/update-submodule.sh Executable file
View File

@ -0,0 +1,69 @@
#!/bin/bash
# Copyright 2021 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.
# Update python-base submodule and collect release notes.
# Usage:
#
# $ scripts/update-submodule.sh
#
# # To update the release notes for a specific release (e.g. v18.17.0a1):
# $ TARGET_RELEASE="v18.17.0a1" scripts/update-submodule.sh
#
# After the script finishes, please create a commit "generated python-base update"
# and send a PR to this repository.
# TODO(roycaihw): make the script send a PR
set -o errexit
set -o nounset
set -o pipefail
repo_root="$(git rev-parse --show-toplevel)"
declare -r repo_root
cd "${repo_root}"
source scripts/util/changelog.sh
source scripts/util/common.sh
util::common::check_sed
go get k8s.io/release/cmd/release-notes
TARGET_RELEASE=${TARGET_RELEASE:-"v$(grep "^CLIENT_VERSION = \"" scripts/constants.py | sed "s/CLIENT_VERSION = \"//g" | sed "s/\"//g")"}
# update submodule
git submodule update --remote
# download release notes
start_sha=$(git diff | grep "^-Subproject commit " | sed 's/-Subproject commit //g')
end_sha=$(git diff | grep "^+Subproject commit " | sed 's/+Subproject commit //g')
output="/tmp/python-base-relnote.md"
release-notes --dependencies=false --org kubernetes-client --repo python-base --start-sha $start_sha --end-sha $end_sha --output $output
sed -i 's/(\[\#/(\[kubernetes-client\/python-base\#/g' $output
# update changelog
IFS_backup=$IFS
IFS=$'\n'
sections=($(grep "^### " $output))
IFS=$IFS_backup
for section in "${sections[@]}"; do
# ignore section titles and empty lines; replace newline with liternal "\n"
release_notes=$(sed -n "/$section/,/###/{/###/!p}" $output | sed -n "{/^$/!p}" | sed ':a;N;$!ba;s/\n/\\n/g')
util::changelog::write_changelog "$TARGET_RELEASE" "$section" "$release_notes"
done
rm -f $output
echo "Successfully updated CHANGELOG for submodule."

109
scripts/util/changelog.sh Executable file
View File

@ -0,0 +1,109 @@
#!/bin/bash
# Copyright 2021 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.
changelog="$(git rev-parse --show-toplevel)/CHANGELOG.md"
function util::changelog::has_release {
local release=$1
return $(grep -q "^# $release$" $changelog)
}
# find_release_start returns the number of the first line of the given release
function util::changelog::find_release_start {
local release=$1
echo $(grep -n "^# $release$" $changelog | head -1 | cut -d: -f1)
}
# find_release_end returns the number of the last line of the given release
function util::changelog::find_release_end {
local release=$1
local release_start=$(util::changelog::find_release_start $release)
local next_release_index=0
local releases=($(grep -n "^# " $changelog | cut -d: -f1))
for i in "${!releases[@]}"; do
if [[ "${releases[$i]}" = "$release_start" ]]; then
next_release_index=$((i+1))
break
fi
done
# return the line before the next release
echo $((${releases[${next_release_index}]}-1))
}
# has_section returns if the given section exists between start and end
function util::changelog::has_section_in_range {
local section="$1"
local start=$2
local end=$3
local lines=($(grep -n "$section" "$changelog" | cut -d: -f1))
for i in "${!lines[@]}"; do
if [[ ${lines[$i]} -ge $start && ${lines[$i]} -le $end ]]; then
return 0
fi
done
return 1
}
# find_section returns the number of the first line of the given section
function util::changelog::find_section_in_range {
local section="$1"
local start=$2
local end=$3
local line="0"
local lines=($(grep -n "$section" "$changelog" | cut -d: -f1))
for i in "${!lines[@]}"; do
if [[ ${lines[$i]} -ge $start && ${lines[$i]} -le $end ]]; then
line=${lines[$i]}
break
fi
done
echo $line
}
# write_changelog writes release_notes to section in target_release
function util::changelog::write_changelog {
local target_release="$1"
local section="$2"
local release_notes="$3"
# find the place in the changelog that we want to edit
local line_to_edit="1"
if util::changelog::has_release $target_release; then
# the target release exists
release_first_line=$(util::changelog::find_release_start $target_release)
release_last_line=$(util::changelog::find_release_end $target_release)
if util::changelog::has_section_in_range "$section" "$release_first_line" "$release_last_line"; then
# prepend to existing section
line_to_edit=$(($(util::changelog::find_section_in_range "$section" "$release_first_line" "$release_last_line")+1))
else
# add a new section; plus 4 so that the section is placed below "Kubernetes API Version"
line_to_edit=$(($(util::changelog::find_release_start $target_release)+4))
release_notes="$section\n$release_notes\n"
fi
else
# add a new release
release_notes="# $target_release\n\nKubernetes API Version: To Be Updated\n\n$section\n$release_notes\n"
fi
echo "Writing the following release notes to CHANGELOG line $line_to_edit:"
echo -e $release_notes
# update changelog
sed -i "${line_to_edit}i${release_notes}" $changelog
}

35
scripts/util/common.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/bash
# Copyright 2021 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.
# check_sed returns an error and suggests installing GNU sed, if OS X sed is
# detected.
function util::common::check_sed {
# OS X sed doesn't support "--version". This way we can tell if OS X sed is
# used.
if ! sed --version &>/dev/null; then
# OS X sed and GNU sed aren't compatible with backup flag "-i". Namely
# sed -i ... - does not work on OS X
# sed -i'' ... - does not work on certain OS X versions
# sed -i '' ... - does not work on GNU
echo ">>> OS X sed detected, which may be incompatible with this script. Please install and use GNU sed instead:
$ brew install gnu-sed
$ brew info gnu-sed
# Find the path to the installed gnu-sed and add it to your PATH. The default
# is:
# PATH=\"/Users/\$USER/homebrew/opt/gnu-sed/libexec/gnubin:\$PATH\""
exit 1
fi
}

View File

@ -16,7 +16,7 @@ from setuptools import setup
# Do not edit these constants. They will be updated automatically
# by scripts/update-client.sh.
CLIENT_VERSION = "18.17.0a1"
CLIENT_VERSION = "18.0.0-snapshot"
PACKAGE_NAME = "kubernetes"
DEVELOPMENT_STATUS = "3 - Alpha"
@ -72,12 +72,10 @@ setup(
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
)

View File

@ -1,7 +1,7 @@
[tox]
envlist =
py27, py3{5,6,7,8}
py27-functional, py3{5,6,7,8}-functional
py3{6,7,8,9}
py3{6,7,8,9}-functional
[testenv]
passenv = TOXENV CI TRAVIS TRAVIS_*