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/test.yaml b/.github/workflows/test.yaml index 51e315c5fd..5813b7dac5 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: [2.7, 3.5, 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 c3cb704903..a218cc4c24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: python -dist: xenial +dist: bionic services: - docker @@ -13,9 +13,16 @@ jobs: include: - stage: verify-tag python: 3.7 + arch: ppc64le script: > [ "v$(python -c 'from scripts.constants import CLIENT_VERSION; print(CLIENT_VERSION)')" == "${TRAVIS_TAG}" ] && [[ "${TRAVIS_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(([ab]|dev|rc)[0-9]+)?$ ]] + - stage: verify-tag + python: 3.7 + script: > + [ "v$(python -c 'from scripts.constants import CLIENT_VERSION; print(CLIENT_VERSION)')" == "${TRAVIS_TAG}" ] && + [[ "${TRAVIS_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(([ab]|dev|rc)[0-9]+)?$ ]] + - stage: test python: 2.7 env: TOXENV=update-pycodestyle @@ -43,8 +50,53 @@ jobs: env: TOXENV=py38 - python: 3.8 env: TOXENV=py38-functional + - python: 3.9 + env: TOXENV=py39 + - python: 3.9 + env: TOXENV=py39-functional + - 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: 2.7 + env: TOXENV=update-pycodestyle + arch: ppc64le + - python: 3.7 + env: TOXENV=docs + arch: ppc64le + - python: 2.7 + env: TOXENV=coverage,codecov + arch: ppc64le + - python: 2.7 + env: TOXENV=py27 + arch: ppc64le + - python: 3.5 + env: TOXENV=py35 + 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__ diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0535b602..38a555ab3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 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) + +**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 Kubernetes API Version: 1.17.14 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/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/pod_exec.py b/examples/pod_exec.py index 6a4bf6bdb5..1e2d9195a6 100644 --- a/examples/pod_exec.py +++ b/examples/pod_exec.py @@ -117,8 +117,11 @@ def exec_commands(api_instance): def main(): config.load_kube_config() - c = Configuration() - c.assert_hostname = False + try: + c = Configuration().get_default_copy() + except AttributeError: + c = Configuration() + c.assert_hostname = False Configuration.set_default(c) core_v1 = core_v1_api.CoreV1Api() 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/__init__.py b/kubernetes/__init__.py index 8399ac787e..768883d1fc 100644 --- a/kubernetes/__init__.py +++ b/kubernetes/__init__.py @@ -22,3 +22,4 @@ import kubernetes.watch import kubernetes.stream import kubernetes.utils +import kubernetes.leaderelection diff --git a/kubernetes/base b/kubernetes/base index 2da2b981ca..060cac10e5 160000 --- a/kubernetes/base +++ b/kubernetes/base @@ -1 +1 @@ -Subproject commit 2da2b981ca806b25487ad92d01a2164815c18517 +Subproject commit 060cac10e53169c904e0d50b7448233829019e35 diff --git a/kubernetes/e2e_test/port_server.py b/kubernetes/e2e_test/port_server.py new file mode 100644 index 0000000000..75d28528be --- /dev/null +++ b/kubernetes/e2e_test/port_server.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +import select +import socketserver +import sys +import threading +import time + + +class PortServer: + def __init__(self, port): + self.port = port + self.server = socketserver.ThreadingTCPServer(('0.0.0.0', port), self.handler) + self.server.daemon_threads = True + self.thread = threading.Thread(target=self.server.serve_forever, + name='Port %s Server' % port) + self.thread.daemon = True + self.thread.start() + + + def handler(self, request, address, server): + threading.current_thread().name = 'Port %s Handler' % self.port + rlist = [request] + echo = b'' + while True: + r, w, _x = select.select(rlist, [request] if echo else [], []) + if r: + data = request.recv(1024) + if not data: + break + echo += data + if w: + echo = echo[request.send(echo):] + + +if __name__ == '__main__': + ports = [] + for port in sys.argv[1:]: + ports.append(PortServer(int(port))) + time.sleep(10 * 60) + diff --git a/kubernetes/e2e_test/test_client.py b/kubernetes/e2e_test/test_client.py index 00e46a3776..82d77eaf0d 100644 --- a/kubernetes/e2e_test/test_client.py +++ b/kubernetes/e2e_test/test_client.py @@ -13,20 +13,28 @@ # under the License. import json +import os import select 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:] @@ -64,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) @@ -129,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) @@ -167,17 +218,64 @@ def test_portforward_raw(self): client = api_client.ApiClient(configuration=self.config) api = core_v1_api.CoreV1Api(client) + with open(os.path.join(os.path.dirname(__file__), 'port_server.py')) as fh: + port_server_py = fh.read() name = 'portforward-raw-' + short_uuid() - pod_manifest = manifest_with_command( - name, - ' '.join(( - '((while true;do nc -l -p 1234 -e /bin/cat; done)&);', - '((while true;do nc -l -p 1235 -e /bin/cat; done)&);', - 'sleep 60', - )) + resp = api.create_namespaced_config_map( + body={ + 'apiVersion': 'v1', + 'kind': 'ConfigMap', + 'metadata': { + 'name': name, + }, + 'data': { + 'port-server.py': port_server_py, + } + }, + namespace='default', + ) + resp = api.create_namespaced_pod( + body={ + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'name': name + }, + 'spec': { + 'containers': [ + { + 'name': 'port-server', + 'image': 'python', + 'command': [ + '/opt/port-server.py', '1234', '1235', + ], + 'volumeMounts': [ + { + 'name': 'port-server', + 'mountPath': '/opt', + 'readOnly': True, + }, + ], + 'startupProbe': { + 'tcpSocket': { + 'port': 1234, + }, + }, + }, + ], + 'volumes': [ + { + 'name': 'port-server', + 'configMap': { + 'name': name, + 'defaultMode': 0o777, + }, + }, + ], + }, + }, + namespace='default', ) - resp = api.create_namespaced_pod(body=pod_manifest, - namespace='default') self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status.phase) @@ -189,6 +287,7 @@ def test_portforward_raw(self): if resp.status.phase != 'Pending': break time.sleep(1) + self.assertEqual(resp.status.phase, 'Running') pf = portforward(api.connect_get_namespaced_pod_portforward, name, 'default', @@ -251,8 +350,8 @@ def test_portforward_raw(self): self.assertIsNone(pf.error(1234)) self.assertIsNone(pf.error(1235)) - resp = api.delete_namespaced_pod(name=name, body={}, - namespace='default') + resp = api.delete_namespaced_pod(name=name, namespace='default') + resp = api.delete_namespaced_config_map(name=name, namespace='default') def test_portforward_http(self): client = api_client.ApiClient(configuration=self.config) @@ -394,6 +493,7 @@ def test_configmap_apis(self): "apiVersion": "v1", "metadata": { "name": name, + "labels": {"e2e-tests": "true"}, }, "data": { "config.json": "{\"command\":\"/usr/bin/mysqld_safe\"}", @@ -417,7 +517,7 @@ def test_configmap_apis(self): resp = api.delete_namespaced_config_map( name=name, body={}, namespace='default') - resp = api.list_namespaced_config_map('default', pretty=True) + resp = api.list_namespaced_config_map('default', pretty=True, label_selector="e2e-tests=true") self.assertEqual([], resp.items) def test_node_apis(self): diff --git a/kubernetes/e2e_test/test_watch.py b/kubernetes/e2e_test/test_watch.py index 3530b8bbdf..134e9c26fd 100644 --- a/kubernetes/e2e_test/test_watch.py +++ b/kubernetes/e2e_test/test_watch.py @@ -32,6 +32,7 @@ def config_map_with_value(name, value): "kind": "ConfigMap", "metadata": { "name": name, + "labels": {"e2e-tests": "true"}, }, "data": { "key": value, @@ -57,7 +58,7 @@ def test_watch_configmaps(self): body=configmap_a, namespace='default') # list all configmaps and extract the resource version - resp = api.list_namespaced_config_map('default') + resp = api.list_namespaced_config_map('default', label_selector="e2e-tests=true") rv = resp.metadata.resource_version # create another configmap @@ -73,7 +74,7 @@ def test_watch_configmaps(self): # delete all configmaps api.delete_collection_namespaced_config_map( - namespace='default') + namespace='default', label_selector="e2e-tests=true") w = watch.Watch() # expect to observe all events happened after the initial LIST @@ -83,7 +84,8 @@ def test_watch_configmaps(self): for event in w.stream(api.list_namespaced_config_map, namespace='default', resource_version=rv, - timeout_seconds=5): + timeout_seconds=5, + label_selector="e2e-tests=true"): self.assertEqual(event['type'], expect[i]) # Kubernetes doesn't guarantee the order of the two objects # being deleted diff --git a/kubernetes/leaderelection b/kubernetes/leaderelection new file mode 120000 index 0000000000..30e0567f73 --- /dev/null +++ b/kubernetes/leaderelection @@ -0,0 +1 @@ +base/leaderelection \ No newline at end of file diff --git a/kubernetes/utils/create_from_yaml.py b/kubernetes/utils/create_from_yaml.py index 390131a5af..35111387df 100644 --- a/kubernetes/utils/create_from_yaml.py +++ b/kubernetes/utils/create_from_yaml.py @@ -20,6 +20,9 @@ from kubernetes import client +UPPER_FOLLOWED_BY_LOWER_RE = re.compile('(.)([A-Z][a-z]+)') +LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE = re.compile('([a-z0-9])([A-Z])') + def create_from_yaml( k8s_client, @@ -155,8 +158,8 @@ def create_from_yaml_single_item( k8s_api = getattr(client, fcn_to_call)(k8s_client) # Replace CamelCased action_type into snake_case kind = yml_object["kind"] - kind = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', kind) - kind = re.sub('([a-z0-9])([A-Z])', r'\1_\2', kind).lower() + kind = UPPER_FOLLOWED_BY_LOWER_RE.sub(r'\1_\2', kind) + kind = LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE.sub(r'\1_\2', kind).lower() # Expect the user to create namespaced objects more often if hasattr(k8s_api, "create_namespaced_{0}".format(kind)): # Decide which namespace we are going to put the object in, 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/update-client.sh b/scripts/update-client.sh index 506038429a..f27ca60d4f 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -21,6 +21,25 @@ set -o errexit set -o nounset set -o pipefail +# The openapi-generator version used by this client +export OPENAPI_GENERATOR_COMMIT="v4.3.0" + +# 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 + SCRIPT_ROOT=$(dirname "${BASH_SOURCE}") CLIENT_ROOT="${SCRIPT_ROOT}/../kubernetes" CLIENT_VERSION=$(python "${SCRIPT_ROOT}/constants.py" CLIENT_VERSION) diff --git a/scripts/update-submodule.sh b/scripts/update-submodule.sh new file mode 100755 index 0000000000..86aa5a4b5a --- /dev/null +++ b/scripts/update-submodule.sh @@ -0,0 +1,65 @@ +#!/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 +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/setup.py b/setup.py index 17656cd278..236f68d9e9 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ 'kubernetes.watch', 'kubernetes.client.api', 'kubernetes.stream', 'kubernetes.client.models', 'kubernetes.utils', 'kubernetes.client.apis', - 'kubernetes.dynamic'], + 'kubernetes.dynamic', 'kubernetes.leaderelection'], include_package_data=True, long_description="Python client for kubernetes http://kubernetes.io/", classifiers=[ @@ -79,5 +79,6 @@ "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..8ba4c3dd8d 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 + py27, py3{5,6,7,8,9} + py27-functional, py3{5,6,7,8,9}-functional [testenv] passenv = TOXENV CI TRAVIS TRAVIS_*