diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..f6af35b429 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,72 @@ + + +#### What type of PR is this? + + + +#### What this PR does / why we need it: + +#### Which issue(s) this PR fixes: + +Fixes # + +#### Special notes for your reviewer: + +#### Does this PR introduce a user-facing change? + +```release-note + +``` + +#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.: + + +```docs + +``` diff --git a/.github/workflows/e2e-master.yaml b/.github/workflows/e2e-master.yaml new file mode 100644 index 0000000000..66c5dc4a43 --- /dev/null +++ b/.github/workflows/e2e-master.yaml @@ -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 diff --git a/.github/workflows/e2e-release-11.0.yaml b/.github/workflows/e2e-release-11.0.yaml new file mode 100644 index 0000000000..1cdac39d59 --- /dev/null +++ b/.github/workflows/e2e-release-11.0.yaml @@ -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 diff --git a/.github/workflows/e2e-release-12.0.yaml b/.github/workflows/e2e-release-12.0.yaml new file mode 100644 index 0000000000..213d4a1af7 --- /dev/null +++ b/.github/workflows/e2e-release-12.0.yaml @@ -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 diff --git a/.github/workflows/e2e-release-17.0.yaml b/.github/workflows/e2e-release-17.0.yaml new file mode 100644 index 0000000000..1cf636aa27 --- /dev/null +++ b/.github/workflows/e2e-release-17.0.yaml @@ -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 diff --git a/.github/workflows/e2e-release-18.0.yaml b/.github/workflows/e2e-release-18.0.yaml new file mode 100644 index 0000000000..a1c594c2e4 --- /dev/null +++ b/.github/workflows/e2e-release-18.0.yaml @@ -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 diff --git a/.github/workflows/kind-configs/cluster-1.15.yaml b/.github/workflows/kind-configs/cluster-1.15.yaml new file mode 100644 index 0000000000..caee5e7759 --- /dev/null +++ b/.github/workflows/kind-configs/cluster-1.15.yaml @@ -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 diff --git a/.github/workflows/kind-configs/cluster-1.16.yaml b/.github/workflows/kind-configs/cluster-1.16.yaml new file mode 100644 index 0000000000..36dfc9401f --- /dev/null +++ b/.github/workflows/kind-configs/cluster-1.16.yaml @@ -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 diff --git a/.github/workflows/kind-configs/cluster-1.17.yaml b/.github/workflows/kind-configs/cluster-1.17.yaml new file mode 100644 index 0000000000..f564f001b2 --- /dev/null +++ b/.github/workflows/kind-configs/cluster-1.17.yaml @@ -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 diff --git a/.github/workflows/kind-configs/cluster-1.18.yaml b/.github/workflows/kind-configs/cluster-1.18.yaml new file mode 100644 index 0000000000..88430383d0 --- /dev/null +++ b/.github/workflows/kind-configs/cluster-1.18.yaml @@ -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 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 51e315c5fd..65640995e0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 diff --git a/.travis.yml b/.travis.yml index 4f40c73db8..daf78dab2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,34 +24,54 @@ 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.9 + env: TOXENV=py39 + - stage: deploy + script: skip + 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: 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-functional + env: TOXENV=py38 + - python: 3.9 + env: TOXENV=py39 + arch: ppc64le - stage: deploy script: skip + arch: ppc64le deploy: provider: pypi user: __token__ diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f6b86634..0597d9868c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v18.20.0b1 + +Kubernetes API Version: 1.18.20 + +**Important Information:** + +- Python 2 had reached [End of Life](https://www.python.org/doc/sunset-python-2/) on January 1, 2020. The Kubernetes Python Client has dropped support for Python 2 from this release (v18.20.0b1) and will no longer provide support to older clients as per the [Kubernetes support policy](https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions). + # v18.17.0a1 Kubernetes API Version: 1.18.17 @@ -47,6 +55,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 diff --git a/README.md b/README.md index f91edc0a23..7b34d555ab 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ supported versions of Kubernetes clusters. - [client 10.y.z](https://pypi.org/project/kubernetes/10.1.0/): Kubernetes 1.13 or below (+-), Kubernetes 1.14 (✓), Kubernetes 1.14 or above (+-) - [client 11.y.z](https://pypi.org/project/kubernetes/11.0.0/): Kubernetes 1.14 or below (+-), Kubernetes 1.15 (✓), Kubernetes 1.16 or above (+-) - [client 12.y.z](https://pypi.org/project/kubernetes/12.0.1/): Kubernetes 1.15 or below (+-), Kubernetes 1.16 (✓), Kubernetes 1.17 or above (+-) -- [client 17.y.z](https://pypi.org/project/kubernetes/17.14.0a1/): Kubernetes 1.16 or below (+-), Kubernetes 1.17 (✓), Kubernetes 1.18 or above (+-) -- [client 18.y.z](https://pypi.org/project/kubernetes/18.17.0a1/): Kubernetes 1.17 or below (+-), Kubernetes 1.18 (✓), Kubernetes 1.19 or above (+-) +- [client 17.y.z](https://pypi.org/project/kubernetes/17.17.0/): Kubernetes 1.16 or below (+-), Kubernetes 1.17 (✓), Kubernetes 1.18 or above (+-) +- [client 18.y.z](https://pypi.org/project/kubernetes/18.20.0b1/): Kubernetes 1.17 or below (+-), Kubernetes 1.18 (✓), Kubernetes 1.19 or above (+-) > See [here](#homogenizing-the-kubernetes-python-client-versions) for an explaination of why there is no v13-v16 release. @@ -119,12 +119,13 @@ between client-python versions. | 9.0 Alpha/Beta | Kubernetes main repo, 1.13 branch | ✗ | | 9.0 | Kubernetes main repo, 1.13 branch | ✗ | | 10.0 Alpha/Beta | Kubernetes main repo, 1.14 branch | ✗ | -| 10.0 | Kubernetes main repo, 1.14 branch | ✓ | +| 10.0 | Kubernetes main repo, 1.14 branch | ✗ | | 11.0 Alpha/Beta | Kubernetes main repo, 1.15 branch | ✗ | | 11.0 | Kubernetes main repo, 1.15 branch | ✓ | | 12.0 Alpha/Beta | Kubernetes main repo, 1.16 branch | ✗ | | 12.0 | Kubernetes main repo, 1.16 branch | ✓ | -| 17.0 Alpha/Beta | Kubernetes main repo, 1.17 branch | ✓ | +| 17.0 Alpha/Beta | Kubernetes main repo, 1.17 branch | ✗ | +| 17.0 | Kubernetes main repo, 1.17 branch | ✓ | | 18.0 Alpha/Beta | Kubernetes main repo, 1.18 branch | ✓ | > See [here](#homogenizing-the-kubernetes-python-client-versions) for an explaination of why there is no v13-v16 release. diff --git a/devel/release.md b/devel/release.md index f438889404..f834fbcc42 100644 --- a/devel/release.md +++ b/devel/release.md @@ -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 ``` diff --git a/devel/submodules.md b/devel/submodules.md index 68ca4eee0e..6edca72752 100644 --- a/devel/submodules.md +++ b/devel/submodules.md @@ -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. diff --git a/examples/cluster_scoped_custom_object.py b/examples/cluster_scoped_custom_object.py new file mode 100644 index 0000000000..d9a5c139dd --- /dev/null +++ b/examples/cluster_scoped_custom_object.py @@ -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() diff --git a/examples/deployment_create.py b/examples/deployment_create.py index ba13440ff8..e17af3b5c9 100644 --- a/examples/deployment_create.py +++ b/examples/deployment_create.py @@ -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 diff --git a/examples/deployment_crud.py b/examples/deployment_crud.py index d95d4f7a15..8ad919b983 100644 --- a/examples/deployment_crud.py +++ b/examples/deployment_crud.py @@ -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 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_instance): + +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() diff --git a/examples/dynamic-client/accept_header.py b/examples/dynamic-client/accept_header.py new file mode 100644 index 0000000000..03373380e4 --- /dev/null +++ b/examples/dynamic-client/accept_header.py @@ -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() \ No newline at end of file diff --git a/examples/dynamic-client/cluster_scoped_custom_resource.py b/examples/dynamic-client/cluster_scoped_custom_resource.py new file mode 100644 index 0000000000..532a763619 --- /dev/null +++ b/examples/dynamic-client/cluster_scoped_custom_resource.py @@ -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() diff --git a/examples/dynamic-client/configmap.py b/examples/dynamic-client/configmap.py new file mode 100644 index 0000000000..15094df6be --- /dev/null +++ b/examples/dynamic-client/configmap.py @@ -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() diff --git a/examples/dynamic-client/deployment_rolling_restart.py b/examples/dynamic-client/deployment_rolling_restart.py new file mode 100644 index 0000000000..8218e9e711 --- /dev/null +++ b/examples/dynamic-client/deployment_rolling_restart.py @@ -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() diff --git a/examples/dynamic-client/namespaced_custom_resource.py b/examples/dynamic-client/namespaced_custom_resource.py new file mode 100644 index 0000000000..c577672a37 --- /dev/null +++ b/examples/dynamic-client/namespaced_custom_resource.py @@ -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() diff --git a/examples/dynamic-client/node.py b/examples/dynamic-client/node.py new file mode 100644 index 0000000000..6dff9f5eac --- /dev/null +++ b/examples/dynamic-client/node.py @@ -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() diff --git a/examples/dynamic-client/replication_controller.py b/examples/dynamic-client/replication_controller.py new file mode 100644 index 0000000000..8c4bd6c21c --- /dev/null +++ b/examples/dynamic-client/replication_controller.py @@ -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() diff --git a/examples/dynamic-client/service.py b/examples/dynamic-client/service.py new file mode 100644 index 0000000000..63206fd001 --- /dev/null +++ b/examples/dynamic-client/service.py @@ -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() diff --git a/examples/job_crud.py b/examples/job_crud.py index b18b152d4d..ab02761a7d 100644 --- a/examples/job_crud.py +++ b/examples/job_crud.py @@ -17,6 +17,7 @@ """ 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): diff --git a/examples/multiple_clusters.py b/examples/multiple_clusters.py index 94b0458cd8..9108596478 100644 --- a/examples/multiple_clusters.py +++ b/examples/multiple_clusters.py @@ -18,9 +18,10 @@ 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(): diff --git a/examples/custom_object.py b/examples/namespaced_custom_object.py similarity index 72% rename from examples/custom_object.py rename to examples/namespaced_custom_object.py index 55e359f5bf..6ed8175992 100644 --- a/examples/custom_object.py +++ b/examples/namespaced_custom_object.py @@ -17,7 +17,7 @@ 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 @@ - 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", diff --git a/examples/node_labels.py b/examples/node_labels.py index 22ac3197ad..f71c8126e5 100644 --- a/examples/node_labels.py +++ b/examples/node_labels.py @@ -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__': diff --git a/examples/pick_kube_config_context.py b/examples/pick_kube_config_context.py index 962639669b..d0e26d62e4 100644 --- a/examples/pick_kube_config_context.py +++ b/examples/pick_kube_config_context.py @@ -18,9 +18,10 @@ 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(): diff --git a/examples/pod_config_list.py b/examples/pod_config_list.py index 09bbde9b69..882b13c592 100644 --- a/examples/pod_config_list.py +++ b/examples/pod_config_list.py @@ -19,9 +19,10 @@ 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(): diff --git a/examples/remote_cluster.py b/examples/remote_cluster.py index b72b39b4e9..84ebeb4f64 100644 --- a/examples/remote_cluster.py +++ b/examples/remote_cluster.py @@ -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 = "" diff --git a/kubernetes/README.md b/kubernetes/README.md index 5f4642649f..13f2126959 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -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.20.0b1 - Build package: org.openapitools.codegen.languages.PythonClientCodegen ## Requirements. diff --git a/kubernetes/__init__.py b/kubernetes/__init__.py index 313907c7e9..2b367f8884 100644 --- a/kubernetes/__init__.py +++ b/kubernetes/__init__.py @@ -14,7 +14,7 @@ __project__ = 'kubernetes' # The version is auto-updated. Please do not edit. -__version__ = "18.17.0a1" +__version__ = "18.20.0b1" import kubernetes.client import kubernetes.config diff --git a/kubernetes/client/__init__.py b/kubernetes/client/__init__.py index e0dba0bad3..b47a5b3dab 100644 --- a/kubernetes/client/__init__.py +++ b/kubernetes/client/__init__.py @@ -14,7 +14,7 @@ from __future__ import absolute_import -__version__ = "18.17.0a1" +__version__ = "18.20.0b1" # import apis into sdk package from kubernetes.client.api.admissionregistration_api import AdmissionregistrationApi diff --git a/kubernetes/client/api_client.py b/kubernetes/client/api_client.py index 9fd29e36f6..edcdead5f6 100644 --- a/kubernetes/client/api_client.py +++ b/kubernetes/client/api_client.py @@ -78,7 +78,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, 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.20.0b1/python' self.client_side_validation = configuration.client_side_validation def __enter__(self): diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index b9170cb164..ed81b5164f 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -347,7 +347,7 @@ def to_debug_report(self): "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.20.0b1".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self): diff --git a/kubernetes/e2e_test/test_client.py b/kubernetes/e2e_test/test_client.py index af71b455a3..1034f0ef0d 100644 --- a/kubernetes/e2e_test/test_client.py +++ b/kubernetes/e2e_test/test_client.py @@ -19,15 +19,22 @@ 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 @@ def test_pod_apis(self): 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 @@ def test_exit_code(self): 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 @@ def test_exit_code(self): 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) diff --git a/requirements.txt b/requirements.txt index 74e38b7cd4..5411efad1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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+ diff --git a/scripts/apply-hotfixes.sh b/scripts/apply-hotfixes.sh index bf866d7564..4d84203c44 100755 --- a/scripts/apply-hotfixes.sh +++ b/scripts/apply-hotfixes.sh @@ -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 diff --git a/scripts/constants.py b/scripts/constants.py index d0bd109eea..beb6dd796c 100644 --- a/scripts/constants.py +++ b/scripts/constants.py @@ -18,13 +18,13 @@ KUBERNETES_BRANCH = "release-1.18" # client version for packaging and releasing. -CLIENT_VERSION = "18.17.0a1" +CLIENT_VERSION = "18.20.0b1" # Name of the release package PACKAGE_NAME = "kubernetes" # Stage of development, mainly used in setup.py's classifiers. -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # If called directly, return the constant value given diff --git a/scripts/update-client.sh b/scripts/update-client.sh index be35be9cfd..a0620a9690 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -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" diff --git a/scripts/update-pycodestyle.sh b/scripts/update-pycodestyle.sh index c84c9825c7..b6075d3751 100755 --- a/scripts/update-pycodestyle.sh +++ b/scripts/update-pycodestyle.sh @@ -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)" diff --git a/scripts/update-submodule.sh b/scripts/update-submodule.sh new file mode 100755 index 0000000000..2ef51eebbc --- /dev/null +++ b/scripts/update-submodule.sh @@ -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." diff --git a/scripts/util/changelog.sh b/scripts/util/changelog.sh new file mode 100755 index 0000000000..672b823369 --- /dev/null +++ b/scripts/util/changelog.sh @@ -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 +} diff --git a/scripts/util/common.sh b/scripts/util/common.sh new file mode 100644 index 0000000000..0955d597b2 --- /dev/null +++ b/scripts/util/common.sh @@ -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 +} diff --git a/setup.py b/setup.py index 3db212a3ba..28014ceeb8 100644 --- a/setup.py +++ b/setup.py @@ -16,9 +16,9 @@ # Do not edit these constants. They will be updated automatically # by scripts/update-client.sh. -CLIENT_VERSION = "18.17.0a1" +CLIENT_VERSION = "18.20.0b1" PACKAGE_NAME = "kubernetes" -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # To install the library, run the following # @@ -72,12 +72,10 @@ "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", ], ) diff --git a/tox.ini b/tox.ini index 0930582f4b..9c7e4b7e09 100644 --- a/tox.ini +++ b/tox.ini @@ -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_*