pax_global_header00006660000000000000000000000064144600435570014522gustar00rootroot0000000000000052 comment=8354d02a535699e2e3879fd93ced3a5e56466503 openshift-restclient-python-0.13.2/000077500000000000000000000000001446004355700172755ustar00rootroot00000000000000openshift-restclient-python-0.13.2/.coveragerc000066400000000000000000000002421446004355700214140ustar00rootroot00000000000000[run] branch = True omit = */lib/python*/site-packages/* */lib/python*/* /usr/* setup.py */conftest.py */test/* [html] directory = cover openshift-restclient-python-0.13.2/.gitignore000066400000000000000000000015461446004355700212730ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover cover/ .hypothesis/ venv*/ .python-version # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints # Intellij IDEA files .idea/* *.iml # Generated Ansible modules _modules/ openshift-restclient-python-0.13.2/.tito/000077500000000000000000000000001446004355700203325ustar00rootroot00000000000000openshift-restclient-python-0.13.2/.tito/packages/000077500000000000000000000000001446004355700221105ustar00rootroot00000000000000openshift-restclient-python-0.13.2/.tito/packages/.readme000066400000000000000000000002371446004355700233500ustar00rootroot00000000000000the .tito/packages directory contains metadata files named after their packages. Each file has the latest tagged version and the project's relative directory. openshift-restclient-python-0.13.2/.tito/packages/python-openshift000066400000000000000000000000131446004355700253430ustar00rootroot000000000000000.8.0-1 ./ openshift-restclient-python-0.13.2/.tito/releasers.conf000066400000000000000000000012301446004355700231620ustar00rootroot00000000000000[asb-copr] releaser = tito.release.CoprReleaser project_name = @ansible-service-broker/ansible-service-broker-latest upload_command = scp -4 %(srpm)s $fas_username@fedorapeople.org:/srv/repos/asb remote_location = http://repos.fedorapeople.org/asb/ copr_options = --timeout 600 [asb-copr-test] releaser = tito.release.CoprReleaser project_name = @ansible-service-broker/ansible-service-broker-nightly upload_command = scp -4 %(srpm)s $fas_username@fedorapeople.org:/srv/repos/asb remote_location = http://repos.fedorapeople.org/asb/ copr_options = --timeout 600 builder.test = 1 [asb-brew] releaser = tito.release.DistGitReleaser branches = rhaos-4.0-asb-rhel-7 openshift-restclient-python-0.13.2/.tito/tito.props000066400000000000000000000002231446004355700223730ustar00rootroot00000000000000[buildconfig] builder = tito.builder.Builder tagger = tito.tagger.ReleaseTagger changelog_do_not_remove_cherrypick = 0 changelog_format = %s (%ae) openshift-restclient-python-0.13.2/.travis.yml000066400000000000000000000033371446004355700214140ustar00rootroot00000000000000sudo: required services: - docker cache: - pip language: python python: - '3.10-dev' env: global: - COVERALLS_PARALLEL=true jobs: - TEST_SUITE=unit script: tox install: - pip install tox-travis coveralls after_success: - coveralls jobs: include: - stage: lint python: '3.10-dev' install: - pip install tox-travis script: tox -e py310-lint env: - TEST_SUITE=lint - stage: deploy script: skip python: '3.10-dev' deploy: provider: pypi user: openshift password: secure: OtT8BL3rmSlag7H+jLNuPl52UOD8HVuohK0d4OQNMTiejZ1ali/fT4K1yCqteLLTalode1KnG/YpqQuOeEEVJCZNqbAeG3VuD4o3sx0hn1ATMwu5RDngovSSVojhKAOMSh/w3TycRysV+x/L5twntGycNUmtDiRGSJFmXGgq0EPLbE6DUv8IMwlhq/ncNz7/RU5KU852kwc6TDHZloHdhOH5ibibi7VCKUlkK/JnAT/OX0uw/j4v6Dvs6CzJ2lrSozUtjP5OxWPh6XRcXNxlxYuTSy7rjqBd05pk3YUAOQt/cEa4581Qu9hSEvhWFSJBjxpggWunTAbeSjqpdY+iqHcoKg+J3ErscIQnsLOwo8nCIKzyn4GMGx/jQYNfWuXGJiOqHFFCyYR16BajQ/gWk78K5MpUDlf2zoyq3NjTp7edY9lWWL7BEgZdd5EY6wI3V7tlGFeNG/iWw2gL3jR+FfUqzEnBnA+EfPUcowLD6rVCsKoxuLs1YyNiOJLzI1lX3FXvntEkVjPH7w5yju2GXWuw/Szgd0MK2HSedV/tp526V/GOkhD1sbQngEVdrIwPI1QbiMX/1pcSvkCvJrKq5EpShZ9iD/D0T92/01x5X3HSx+R3yV3zhB8L3H7RDL/F3HGNbqVPaj9rHAfTJaxWhy6Y3nWpfwQI43cKFCNwF28= on: tags: true repo: openshift/openshift-restclient-python condition: "$TRAVIS_TAG =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+(([ab]|dev|rc)[0-9]+)?$" - stage: test-deploy python: '3.10-dev' script: python -c "import openshift ; print(openshift.__version__)" install: - pip install openshift stages: # - lint # - test - name: deploy if: (tag is present) and (type = push) - name: test-deploy if: (tag is present) and (type = push) notifications: webhooks: https://coveralls.io/webhook openshift-restclient-python-0.13.2/Dockerfile000066400000000000000000000002351446004355700212670ustar00rootroot00000000000000FROM python:2.7 RUN mkdir /src WORKDIR /src COPY requirements.txt /src/requirements.txt RUN pip install -r requirements.txt COPY . /src RUN pip install . openshift-restclient-python-0.13.2/LICENSE000066400000000000000000000261351446004355700203110ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. openshift-restclient-python-0.13.2/Makefile000066400000000000000000000002751446004355700207410ustar00rootroot00000000000000.PHONY: test ARTIFACT_DIR ?= ./ test: test-lint test-unit test-integration test-lint: flake8 . test-unit: pytest test/unit -v -r s test-integration: pytest test/integration -v -r s openshift-restclient-python-0.13.2/OWNERS000066400000000000000000000001461446004355700202360ustar00rootroot00000000000000approvers: - fabianvf - jmrodri - willthames reviewers: - fabianvf - jmrodri - willthames openshift-restclient-python-0.13.2/README.md000066400000000000000000000335341446004355700205640ustar00rootroot00000000000000OpenShift python client ====================== [![Build Status](https://travis-ci.com/openshift/openshift-restclient-python.svg?branch=master)](https://travis-ci.com/openshift/openshift-restclient-python) [![Coverage Status](https://coveralls.io/repos/github/openshift/openshift-restclient-python/badge.svg?branch=master)](https://coveralls.io/github/openshift/openshift-restclient-python?branch=master) Python client for the [Kubernetes](https://kubernetes.io/) and [OpenShift](http://openshift.redhat.com/) APIs. There are two ways this project interacts with the Kubernetes and OpenShift APIs. The first, **now deprecated**, is to use models and functions generated with swagger from the API spec. The second, new approach, is to use a single model and client to generically interact with all resources on the server. The dynamic client also works with resources that are defined by aggregated API servers or Custom Resource Definitions. # Table of Contents * [Installation](#installation) * [Usage](#usage) * [Examples](#examples) * [Create a Service](#create-a-service) * [Create a Route](#create-a-route) * [List Projects](#list-projects) * [Custom Resources](#custom-resources) * [OpenShift Login with username and password](#openshift-login-with-username-and-password) * [Available Methods for Resources](#available-methods-for-resources) * [Get](#get) * [Create](#create) * [Delete](#delete) * [Patch](#patch) * [Replace](#replace) * [Watch](#watch) * [Community, Support, Discussion](#community-support-discussion) * [Code of Conduct](#code-of-conduct) # Installation From source: ``` git clone https://github.com/openshift/openshift-restclient-python.git cd openshift-restclient-python python setup.py install ``` From [PyPi](https://pypi.python.org/pypi/openshift/) directly: ``` pip install openshift ``` Using [Dockerfile](Dockerfile): ``` docker build -t openshift-restclient-python -f Dockerfile . ``` # Usage The OpenShift client depends on the [Kubernetes Python client](https://github.com/kubernetes-incubator/client-python.git), and as part of the installation process, the Kubernetes (K8s) client is automatically installed. In the case you are using Docker, you will likely need to share your `.kube/config` with the `openshift-restclient-python` container: ``` docker run -it -v $HOME/.kube/config:/root/.kube/config:z openshift-restclient-python python ``` To work with the dynamic client, you will need an instantiated Kubernetes client object. The Kubernetes client object requires a Kubernetes Config that can be set in the [Config class](https://github.com/kubernetes-client/python/blob/master/kubernetes/client/configuration.py) or using a helper utility. All of the examples that follow make use of the `new_client_from_config()` helper utility provided by the [Kubernetes Client Config](https://github.com/kubernetes-client/python-base/blob/master/config/kube_config.py) that returns an API client to be used with any API object. There are plenty of [Kubernetes Client examples](https://github.com/kubernetes-client/python/tree/master/examples) to examine other ways of accessing Kubernetes Clusters. # Examples ## Create a Service ```python import yaml from kubernetes import client, config from openshift.dynamic import DynamicClient k8s_client = config.new_client_from_config() dyn_client = DynamicClient(k8s_client) v1_services = dyn_client.resources.get(api_version='v1', kind='Service') service = """ kind: Service apiVersion: v1 metadata: name: my-service spec: selector: app: MyApp ports: - protocol: TCP port: 8080 targetPort: 9376 """ service_data = yaml.load(service) resp = v1_services.create(body=service_data, namespace='default') # resp is a ResourceInstance object print(resp.metadata) ``` ## Create a Route Now, we create a Route object, and associate it with the Service from our previous example: ```python import yaml from kubernetes import client, config from openshift.dynamic import DynamicClient k8s_client = config.new_client_from_config() dyn_client = DynamicClient(k8s_client) v1_routes = dyn_client.resources.get(api_version='route.openshift.io/v1', kind='Route') route = """ apiVersion: route.openshift.io/v1 kind: Route metadata: name: frontend spec: host: www.example.com to: kind: Service name: my-service """ route_data = yaml.load(route) resp = v1_routes.create(body=route_data, namespace='default') # resp is a ResourceInstance object print(resp.metadata) ``` ## List Projects The following uses the dynamic client to list Projects the user can access: ```python from kubernetes import client, config from openshift.dynamic import DynamicClient k8s_client = config.new_client_from_config() dyn_client = DynamicClient(k8s_client) v1_projects = dyn_client.resources.get(api_version='project.openshift.io/v1', kind='Project') project_list = v1_projects.get() for project in project_list.items: print(project.metadata.name) ``` ## Custom Resources In the following example, we first create a Custom Resource Definition for `foos.bar.com`, then create an `Foo` resource, and finally get a list of `Foo` resources: ```python import yaml from kubernetes import client, config from openshift.dynamic import DynamicClient k8s_client = config.new_client_from_config() dyn_client = DynamicClient(k8s_client) custom_resources = dyn_client.resources.get( api_version='apiextensions.k8s.io/v1beta1', kind='CustomResourceDefinition' ) # Define the Foo Resource foo_crd = """ kind: CustomResourceDefinition apiVersion: apiextensions.k8s.io/v1beta1 metadata: name: foos.bar.com spec: group: bar.com names: kind: Foo listKind: FooList plural: foos shortNames: - foo singular: foo scope: Namespaced version: v1beta1 """ custom_resources.create(body=yaml.load(foo_crd)) foo_resources = None while not foo_resources: try: # Notice the re-instantiation of the dynamic client as a new resource has been created. dyn_client = DynamicClient(k8s_client) foo_resources = dyn_client.resources.get(api_version='bar.com/v1beta1', kind='Foo') except: pass # Create the Foo Resource foo_resource_cr = """ kind: Foo apiVersion: bar.com/v1beta1 metadata: name: example-foo namespace: default spec: version: 1 """ foo_resources.create(body=yaml.load(foo_resource_cr)) for item in foo_resources.get().items: print(item.metadata.name) ``` ## OpenShift Login with username and password ```python from kubernetes import client from openshift.dynamic import DynamicClient from openshift.helper.userpassauth import OCPLoginConfiguration apihost = 'https://api.cluster.example.com:6443' username = 'demo-user' password = 'insecure' kubeConfig = OCPLoginConfiguration(ocp_username=username, ocp_password=password) kubeConfig.host = apihost kubeConfig.verify_ssl = True kubeConfig.ssl_ca_cert = './ocp.pem' # use a certificate bundle for the TLS validation # Retrieve the auth token kubeConfig.get_token() print('Auth token: {0}'.format(kubeConfig.api_key)) print('Token expires: {0}'.format(kubeConfig.api_key_expires)) k8s_client = client.ApiClient(kubeConfig) dyn_client = DynamicClient(k8s_client) v1_projects = dyn_client.resources.get(api_version='project.openshift.io/v1', kind='Project') project_list = v1_projects.get() for project in project_list.items: print(project.metadata.name) # Renew the auth token kubeConfig.get_token() print('Auth token: {0}'.format(kubeConfig.api_key)) print('Token expires: {0}'.format(kubeConfig.api_key_expires)) ``` # Available Methods for Resources The generic Resource class supports the following methods, though every resource kind does not support every method. ## Get `get(name=None, namespace=None, label_selector=None, field_selector=None, **kwargs)` Query for a resource in the cluster. Will return a `ResourceInstance` object or raise a `NotFoundError` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') # Gets the specific Service named 'example' from the 'test' namespace v1_services.get(name='example', namespace='test') # Lists all Services in the 'test' namespace v1_services.get(namespace='test') # Lists all Services in the cluster (requires high permission level) v1_services.get() # Gets all Services in the 'test' namespace with the 'app' label set to 'foo' v1_services.get(namespace='test', label_selector='app=foo') # Gets all Services except for those in the 'default' namespace v1_services.get(field_selector='metadata.namespace!=default') ``` `get(body=None, namespace=None, **kwargs)` Query for a resource in the cluster. Will return a `ResourceInstance` object or raise a `NotFoundError` For List kind resources (ie, the resource name ends in `List`), the `get` implementation is slightly different. Rather than taking a name, they take a `*List` kind definition and call `get` for each definition in the list. ```python v1_service_list = dyn_client.resources.get(api_version='v1', kind='ServiceList') body = { 'kind': 'ServiceList', 'apiVersion': 'v1', 'items': [ 'metadata': {'name': 'my-service'}, 'spec': { 'selector': {'app': 'MyApp'}, 'ports': [{ 'protocol': 'TCP', 'port': '8080', 'targetPort': '9376' }] } ], # More definitions would go here } # Gets the specified Service(s) from the 'test' namespace v1_service_list.get(body=body, namespace='test') # Lists all Services in the 'test' namespace v1_service_list.get(namespace='test') # Lists all Services in the cluster (requires high permission level) v1_service_list.get() ``` ## Create `create(body=None, namespace=None, **kwargs)` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') body = { 'kind': 'Service', 'apiVersion': 'v1', 'metadata': {'name': 'my-service'}, 'spec': { 'selector': {'app': 'MyApp'}, 'ports': [{ 'protocol': 'TCP', 'port': '8080', 'targetPort': '9376' }] } } # Creates the above service in the 'test' namespace v1_services.create(body=body, namespace='test') ``` The `create` implementation is the same for `*List` kinds, except that each definition in the list will be created separately. If the resource is namespaced (ie, not cluster-level), then one of `namespace`, `label_selector`, or `field_selector` is required. If the resource is cluster-level, then one of `name`, `label_selector`, or `field_selector` is required. ## Delete `delete(name=None, namespace=None, label_selector=None, field_selector=None, **kwargs)` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') # Deletes the specific Service named 'example' from the 'test' namespace v1_services.delete(name='my-service', namespace='test') # Deletes all Services in the 'test' namespace v1_services.delete(namespace='test') # Deletes all Services in the 'test' namespace with the 'app' label set to 'foo' v1_services.delete(namespace='test', label_selector='app=foo') # Deletes all Services except for those in the 'default' namespace v1_services.delete(field_selector='metadata.namespace!=default') ``` `delete(body=None, namespace=None, **kwargs)` For List kind resources (ie, the resource name ends in `List`), the `delete` implementation is slightly different. Rather than taking a name, they take a `*List` kind definition and call `delete` for each definition in the list. ```python v1_service_list = dyn_client.resources.get(api_version='v1', kind='ServiceList') body = { 'kind': 'ServiceList', 'apiVersion': 'v1', 'items': [ 'metadata': {'name': 'my-service'}, 'spec': { 'selector': {'app': 'MyApp'}, 'ports': [{ 'protocol': 'TCP', 'port': '8080', 'tardeletePort': '9376' }] } ], # More definitions would go here } # deletes the specified Service(s) from the 'test' namespace v1_service_list.delete(body=body, namespace='test') # Deletes all Services in the 'test' namespace v1_service_list.delete(namespace='test') ``` ## Patch `patch(body=None, namespace=None, **kwargs)` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') body = { 'kind': 'Service', 'apiVersion': 'v1', 'metadata': {'name': 'my-service'}, 'spec': { 'selector': {'app': 'MyApp2'}, } } # patchs the above service in the 'test' namespace v1_services.patch(body=body, namespace='test') ``` The `patch` implementation is the same for `*List` kinds, except that each definition in the list will be patched separately. ## Replace `replace(body=None, namespace=None, **kwargs)` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') body = { 'kind': 'Service', 'apiVersion': 'v1', 'metadata': {'name': 'my-service'}, 'spec': { 'selector': {'app': 'MyApp2'}, 'ports': [{ 'protocol': 'TCP', 'port': '8080', 'targetPort': '9376' }] } } # replaces the above service in the 'test' namespace v1_services.replace(body=body, namespace='test') ``` The `replace` implementation is the same for `*List` kinds, except that each definition in the list will be replaced separately. ## Watch `watch(namespace=None, name=None, label_selector=None, field_selector=None, resource_version=None, timeout=None)` ```python v1_services = dyn_client.resources.get(api_version='v1', kind='Service') # Prints the resource that triggered each event related to Services in the 'test' namespace for event in v1_services.watch(namespace='test'): print(event['object']) ``` # Community, Support, Discussion If you have any problem with the package or any suggestions, please file an [issue](https://github.com/openshift/openshift-restclient-python/issues). ## Code of Conduct Participation in the Kubernetes community is governed by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). openshift-restclient-python-0.13.2/RELEASE.md000066400000000000000000000031621446004355700207010ustar00rootroot00000000000000# Instructions for creating a release - [ ] Check out the release branch. Release branches are named `release-{MAJOR}.{MINOR}`. - [ ] Update `CLIENT_VERSION` in [`scripts/constants.py`](scripts/constants.py#L26) to reflect the new version. - [ ] Update `KUBERNETES_CLIENT_VERSION` in [`scripts/constants.py`](scripts/constants.py#L27) to reflect an updated `kubernetes` python package, if needed. - [ ] Run [`scripts/update_version.sh`](scripts/update_version.sh). This will update the version numbers everywhere they are referenced. - [ ] Commit the changes made by the previous step. If you have push access, you can push the branch up directly. If not, you can open a PR and a maintainer will review and merge it. - [ ] In the GitHub UI go to the [`releases`](https://github.com/openshift/openshift-restclient-python/releases) tab. Select [`Draft a new release`](https://github.com/openshift/openshift-restclient-python/releases/new). - [ ] For tag version, fill in `v` followed by the value for `CLIENT_VERSION` that you added to [`constants.py`](scripts/constants.py#L26), ie, if your `CLIENT_VERSION` is `0.36.2`, your tag will be `v0.36.2` - [ ] Ensure that the `Target` is set to the proper `release-{MAJOR}.{MINOR}`, NOT master. - [ ] For `Release title`, just put `Release` followed by the new version you set in `CLIENT_VERSION` - [ ] For the release notes, summarize the features and bugfixes that have been made since the last release. There is no automation for this part yet unfortunately. - [ ] Click `Publish release`. This will kick off a travis-ci build, and once that succeeds the package will be uploaded to pypi automatically. openshift-restclient-python-0.13.2/doc/000077500000000000000000000000001446004355700200425ustar00rootroot00000000000000openshift-restclient-python-0.13.2/doc/proposals/000077500000000000000000000000001446004355700220645ustar00rootroot00000000000000openshift-restclient-python-0.13.2/doc/proposals/apply.md000066400000000000000000000073121446004355700235360ustar00rootroot00000000000000# Add Apply to Python Client ## Problem Current merge/patch strategy in the Ansible modules is not sufficient for a variety of reasons. 1. There is no way to remove a field from the Ansible module without doing a full replace on the object 2. The idempotence of the module is entirely reliant on the idempotence of the server. If a task to create a resource is run twice, two `GET`s and two `PATCH`es will be issued, even if there was no content change. This is particularly an issue for the Operater use-case, as tasks will be run repeatedly every 8 seconds or so, and the `PATCH` requests are not cacheable. 3. There are several core resources that a blind, full-object patch does not work well with, including Deployments or any resource that has a `ports` field, as the default merge strategy of `strategic-merge-patch` is bugged on the server for those resources. 4. The `strategic-merge-patch` also does not work at all for CustomResources, requiring the user to add a directive to use a different patch strategy for CustomResource operations. ## Proposal Mimic the logic from `kubectl apply` to allow more intelligent patch strategies in the client. This implementation would be a stop-gap, as `apply` is slated to move to the server at some undefined point in the future, at which point we would probably just use the server-side apply. `kubectl apply` makes use of a fairly complex 3-way diffing algorithm to generate a patch that performs deletions between the new configuration and the `LastAppliedConfiguration` (stored in the `kubectl.kubernetes.io/last-applied-configuration` annotation), while preserving non-conflicting additions to the real state of the object (ie, system defaulting or manual changes made to pieces of the spec). This algorithm is fairly well-documented and cleanly written, but has a large number of edge cases and a fairly high branching factor. The basic algorithm is as follows: Given: a desired definition for an object to apply to the cluster 1. `GET` the current state of the object from Kubernetes 2. Pull the `LastAppliedConfiguration` from the current object 3. If the `LastAppliedConfiguration` does not exist, diff the desired definition and the current object and send the resulting patch to the server. 4. Otherwise, diff the `LastAppliedConfiguration` and the desired definition to compute the deletions. 5. diff the current object and the desired definition to get the delta 6. Combine the deletions (from step 4) and the delta (from step 5) to a single patch 7. If the patch is not empty, send it to the server Much of the complexity comes from diffing complex nested objects, and a variety of patch strategies that may be used (ie, adding vs replacing lists, reordering of lists or keys, deep vs shallow object comparisons) Resources for golang implementation: - https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#kubectl-apply - https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/apply/apply.go - https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/apply.go - https://github.com/kubernetes/apimachinery/blob/master/pkg/util/strategicpatch/patch.go#L2011 ## Estimates Based on a cursory exploration of the golang implementation of `apply`: - Up to 1 week for a basic prototype implementation, that can handle the majority of definitions - 1 week to test/harden against edge cases - 1 week to update and test the Ansible modules. This week may overlap with the hardening/testing period, since the process of integration will likely reveal many common edge-cases Total: 2-3 weeks. I would be surprised if it took any more time than that for a working implementation. openshift-restclient-python-0.13.2/doc/source/000077500000000000000000000000001446004355700213425ustar00rootroot00000000000000openshift-restclient-python-0.13.2/doc/source/conf.py000077500000000000000000000050201446004355700226410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. import os import sys from recommonmark.parser import CommonMarkParser sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- source_parsers = { '.md': CommonMarkParser, } source_suffix = ['.rst', '.md'] # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', # 'sphinx.ext.intersphinx', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'openshift-restclient-python' copyright = u'2017, Red Hat, Inc.' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, u'OpenShift', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. # intersphinx_mapping = {'http://docs.python.org/': None} openshift-restclient-python-0.13.2/doc/source/contributing.rst000066400000000000000000000000551446004355700246030ustar00rootroot00000000000000============ Contributing ============ TODO openshift-restclient-python-0.13.2/doc/source/index.rst000066400000000000000000000010441446004355700232020ustar00rootroot00000000000000.. openshift-restclient-python documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to openshift-restclient-python's documentation! ======================================================== Contents: .. toctree:: :maxdepth: 2 readme installation usage modules contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` openshift-restclient-python-0.13.2/doc/source/installation.rst000066400000000000000000000003041446004355700245720ustar00rootroot00000000000000============ Installation ============ At the command line:: $ pip install openshift Or, if you have virtualenvwrapper installed:: $ mkvirtualenv openshift $ pip install openshift openshift-restclient-python-0.13.2/doc/source/modules.rst000066400000000000000000000001011446004355700235340ustar00rootroot00000000000000openshift ========== .. toctree:: :maxdepth: 4 openshift openshift-restclient-python-0.13.2/doc/source/readme.rst000066400000000000000000000000621446004355700233270ustar00rootroot00000000000000====== Readme ====== .. include:: ../../README.md openshift-restclient-python-0.13.2/doc/source/usage.rst000066400000000000000000000001411446004355700231740ustar00rootroot00000000000000======== Usage ======== To use openshift-restclient-python in a project:: import openshift openshift-restclient-python-0.13.2/openshift/000077500000000000000000000000001446004355700212745ustar00rootroot00000000000000openshift-restclient-python-0.13.2/openshift/__init__.py000066400000000000000000000013471446004355700234120ustar00rootroot00000000000000# Copyright 2016 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Do not edit these constants. They will be updated automatically # by scripts/update-version.sh. __version__ = "0.13.2" __k8s_client_version__ = "21.7.0" openshift-restclient-python-0.13.2/openshift/dynamic/000077500000000000000000000000001446004355700227205ustar00rootroot00000000000000openshift-restclient-python-0.13.2/openshift/dynamic/__init__.py000066400000000000000000000000361446004355700250300ustar00rootroot00000000000000from .client import * # NOQA openshift-restclient-python-0.13.2/openshift/dynamic/apply.py000066400000000000000000000253271446004355700244300ustar00rootroot00000000000000from collections import OrderedDict from copy import deepcopy import json import sys from openshift.dynamic.exceptions import NotFoundError, ApplyException LAST_APPLIED_CONFIG_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration' POD_SPEC_SUFFIXES = { 'containers': 'name', 'initContainers': 'name', 'ephemeralContainers': 'name', 'volumes': 'name', 'imagePullSecrets': 'name', 'containers.volumeMounts': 'mountPath', 'containers.volumeDevices': 'devicePath', 'containers.env': 'name', 'containers.ports': 'containerPort', 'initContainers.volumeMounts': 'mountPath', 'initContainers.volumeDevices': 'devicePath', 'initContainers.env': 'name', 'initContainers.ports': 'containerPort', 'ephemeralContainers.volumeMounts': 'mountPath', 'ephemeralContainers.volumeDevices': 'devicePath', 'ephemeralContainers.env': 'name', 'ephemeralContainers.ports': 'containerPort', } POD_SPEC_PREFIXES = [ 'Pod.spec', 'Deployment.spec.template.spec', 'DaemonSet.spec.template.spec', 'StatefulSet.spec.template.spec', 'Job.spec.template.spec', 'Cronjob.spec.jobTemplate.spec.template.spec', ] # patch merge keys taken from generated.proto files under # staging/src/k8s.io/api in kubernetes/kubernetes STRATEGIC_MERGE_PATCH_KEYS = { 'Service.spec.ports': 'port', 'ServiceAccount.secrets': 'name', 'ValidatingWebhookConfiguration.webhooks': 'name', 'MutatingWebhookConfiguration.webhooks': 'name', } STRATEGIC_MERGE_PATCH_KEYS.update( {"%s.%s" % (prefix, key): value for prefix in POD_SPEC_PREFIXES for key, value in POD_SPEC_SUFFIXES.items()} ) if sys.version_info.major >= 3: json_loads_byteified = json.loads else: # https://stackoverflow.com/a/33571117 def json_loads_byteified(json_text): return _byteify( json.loads(json_text, object_hook=_byteify), ignore_dicts=True ) def _byteify(data, ignore_dicts = False): # if this is a unicode string, return its string representation if isinstance(data, unicode): # noqa: F821 return data.encode('utf-8') # if this is a list of values, return list of byteified values if isinstance(data, list): return [ _byteify(item, ignore_dicts=True) for item in data ] # if this is a dictionary, return dictionary of byteified keys and values # but only if we haven't already byteified it if isinstance(data, dict) and not ignore_dicts: return { _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True) for key, value in data.items() } # if it's anything else, return it in its original form return data def annotate(desired): return dict( metadata=dict( annotations={ LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(desired, separators=(',', ':'), indent=None, sort_keys=True) } ) ) def apply_patch(actual, desired): last_applied = actual['metadata'].get('annotations',{}).get(LAST_APPLIED_CONFIG_ANNOTATION) if last_applied: # ensure that last_applied doesn't come back as a dict of unicode key/value pairs # json.loads can be used if we stop supporting python 2 last_applied = json_loads_byteified(last_applied) patch = merge(dict_merge(last_applied, annotate(last_applied)), dict_merge(desired, annotate(desired)), actual) if patch: return actual, patch else: return actual, actual else: return actual, dict_merge(desired, annotate(desired)) def apply_object(resource, definition): try: actual = resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace')) except NotFoundError: return None, dict_merge(definition, annotate(definition)) return apply_patch(actual.to_dict(), definition) def apply(resource, definition): existing, desired = apply_object(resource, definition) if not existing: return resource.create(body=desired, namespace=definition['metadata'].get('namespace')) if existing == desired: return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace')) return resource.patch(body=desired, name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'), content_type='application/merge-patch+json') # The patch is the difference from actual to desired without deletions, plus deletions # from last_applied to desired. To find it, we compute deletions, which are the deletions from # last_applied to desired, and delta, which is the difference from actual to desired without # deletions, and then apply delta to deletions as a patch, which should be strictly additive. def merge(last_applied, desired, actual, position=None): deletions = get_deletions(last_applied, desired) delta = get_delta(last_applied, actual, desired, position or desired['kind']) return dict_merge(deletions, delta) # dict_merge taken from Ansible's module_utils.common.dict_transformations def dict_merge(a, b): '''recursively merges dicts. not just simple a['key'] = b['key'], if both a and b have a key whose value is a dict then dict_merge is called on both values and the result stored in the returned dictionary.''' if not isinstance(b, dict): return b result = deepcopy(a) for k, v in b.items(): if k in result and isinstance(result[k], dict): result[k] = dict_merge(result[k], v) else: result[k] = deepcopy(v) return result def list_to_dict(lst, key, position): result = OrderedDict() for item in lst: try: result[item[key]] = item except KeyError: raise ApplyException("Expected key '%s' not found in position %s" % (key, position)) return result # list_merge applies a strategic merge to a set of lists if the patchMergeKey is known # each item in the list is compared based on the patchMergeKey - if two values with the # same patchMergeKey differ, we take the keys that are in last applied, compare the # actual and desired for those keys, and update if any differ def list_merge(last_applied, actual, desired, position): result = list() if position in STRATEGIC_MERGE_PATCH_KEYS and last_applied: patch_merge_key = STRATEGIC_MERGE_PATCH_KEYS[position] last_applied_dict = list_to_dict(last_applied, patch_merge_key, position) actual_dict = list_to_dict(actual, patch_merge_key, position) desired_dict = list_to_dict(desired, patch_merge_key, position) for key in desired_dict: if key not in actual_dict or key not in last_applied_dict: result.append(desired_dict[key]) else: patch = merge(last_applied_dict[key], desired_dict[key], actual_dict[key], position) result.append(dict_merge(actual_dict[key], patch)) for key in actual_dict: if key not in desired_dict and key not in last_applied_dict: result.append(actual_dict[key]) return result else: return desired def recursive_list_diff(list1, list2, position=None): result = (list(), list()) if position in STRATEGIC_MERGE_PATCH_KEYS: patch_merge_key = STRATEGIC_MERGE_PATCH_KEYS[position] dict1 = list_to_dict(list1, patch_merge_key, position) dict2 = list_to_dict(list2, patch_merge_key, position) dict1_keys = set(dict1.keys()) dict2_keys = set(dict2.keys()) for key in dict1_keys - dict2_keys: result[0].append(dict1[key]) for key in dict2_keys - dict1_keys: result[1].append(dict2[key]) for key in dict1_keys & dict2_keys: diff = recursive_diff(dict1[key], dict2[key], position) if diff: # reinsert patch merge key to relate changes in other keys to # a specific list element diff[0].update({patch_merge_key: dict1[key][patch_merge_key]}) diff[1].update({patch_merge_key: dict2[key][patch_merge_key]}) result[0].append(diff[0]) result[1].append(diff[1]) if result[0] or result[1]: return result elif list1 != list2: return (list1, list2) return None def recursive_diff(dict1, dict2, position=None): if not position: if 'kind' in dict1 and dict1.get('kind') == dict2.get('kind'): position = dict1['kind'] left = dict((k, v) for (k, v) in dict1.items() if k not in dict2) right = dict((k, v) for (k, v) in dict2.items() if k not in dict1) for k in (set(dict1.keys()) & set(dict2.keys())): this_position = "%s.%s" % (position, k) if position is not None else None if isinstance(dict1[k], dict) and isinstance(dict2[k], dict): result = recursive_diff(dict1[k], dict2[k], this_position) if result: left[k] = result[0] right[k] = result[1] elif isinstance(dict1[k], list) and isinstance(dict2[k], list): result = recursive_list_diff(dict1[k], dict2[k], this_position) if result: left[k] = result[0] right[k] = result[1] elif dict1[k] != dict2[k]: left[k] = dict1[k] right[k] = dict2[k] if left or right: return left, right else: return None def get_deletions(last_applied, desired): patch = {} for k, last_applied_value in last_applied.items(): desired_value = desired.get(k) if isinstance(last_applied_value, dict) and isinstance(desired_value, dict): p = get_deletions(last_applied_value, desired_value) if p: patch[k] = p elif last_applied_value != desired_value: patch[k] = desired_value return patch def get_delta(last_applied, actual, desired, position=None): patch = {} for k, desired_value in desired.items(): if position: this_position = "%s.%s" % (position, k) actual_value = actual.get(k) if actual_value is None: patch[k] = desired_value elif isinstance(desired_value, dict): p = get_delta(last_applied.get(k, {}), actual_value, desired_value, this_position) if p: patch[k] = p elif isinstance(desired_value, list): p = list_merge(last_applied.get(k, []), actual_value, desired_value, this_position) if p: patch[k] = [item for item in p if item] elif actual_value != desired_value: patch[k] = desired_value return patch openshift-restclient-python-0.13.2/openshift/dynamic/client.py000066400000000000000000000035541446004355700245570ustar00rootroot00000000000000from kubernetes.dynamic.client import meta_request # noqa from kubernetes.dynamic.client import DynamicClient as K8sDynamicClient from .apply import apply from .discovery import EagerDiscoverer, LazyDiscoverer from .resource import Resource, ResourceList, Subresource, ResourceInstance, ResourceField from .exceptions import ApplyException try: import kubernetes_validate # noqa HAS_KUBERNETES_VALIDATE = True except ImportError: HAS_KUBERNETES_VALIDATE = False try: from kubernetes_validate.utils import VersionNotSupportedError except ImportError: class VersionNotSupportedError(NotImplementedError): pass __all__ = [ 'DynamicClient', 'ResourceInstance', 'Resource', 'ResourceList', 'Subresource', 'EagerDiscoverer', 'LazyDiscoverer', 'ResourceField', ] class DynamicClient(K8sDynamicClient): """ A kubernetes client that dynamically discovers and interacts with the kubernetes API """ def __init__(self, client, cache_file=None, discoverer=None): discoverer = discoverer or LazyDiscoverer K8sDynamicClient.__init__(self, client, cache_file=cache_file, discoverer=discoverer) def apply(self, resource, body=None, name=None, namespace=None): body = self.serialize_body(body) body['metadata'] = body.get('metadata', dict()) name = name or body['metadata'].get('name') if not name: raise ValueError("name is required to apply {}.{}".format(resource.group_version, resource.kind)) if resource.namespaced: body['metadata']['namespace'] = self.ensure_namespace(resource, namespace, body) try: return apply(resource, body) except ApplyException as e: raise ValueError("Could not apply strategic merge to %s/%s: %s" % (body['kind'], body['metadata']['name'], e)) openshift-restclient-python-0.13.2/openshift/dynamic/discovery.py000066400000000000000000000174301446004355700253060ustar00rootroot00000000000000import os import six import hashlib import tempfile from collections import defaultdict from urllib3.exceptions import ProtocolError, MaxRetryError from kubernetes.dynamic.discovery import Discoverer as K8sDiscoverer from kubernetes.dynamic.discovery import LazyDiscoverer as K8sLazyDiscoverer from kubernetes.dynamic.discovery import LazyDiscoverer as K8sEagerDiscoverer from kubernetes.dynamic.discovery import ( # noqa CacheEncoder, CacheDecoder, DISCOVERY_PREFIX, ResourceGroup, ) from kubernetes.dynamic.exceptions import ( ApiException, ResourceNotFoundError, ResourceNotUniqueError, ServiceUnavailableError, ) from .resource import Resource, ResourceList class Discoverer(K8sDiscoverer): """ A convenient container for storing discovered API resources. Allows easy searching and retrieval of specific resources. Subclasses implement the abstract methods with different loading strategies. """ def __init__(self, client, cache_file): self.client = client default_cachefile_name = 'osrcp-{0}.json'.format(hashlib.sha1(self.__get_default_cache_id()).hexdigest()) self.__cache_file = cache_file or os.path.join(tempfile.gettempdir(), default_cachefile_name) self.__init_cache() def __get_default_cache_id(self): user = self.__get_user() if user: cache_id = "{0}-{1}".format(self.client.configuration.host, user) else: cache_id = self.client.configuration.host if six.PY3: return cache_id.encode('utf-8') return cache_id def __get_user(self): if hasattr(os, 'getlogin'): try: user = os.getlogin() if user: return str(user) except OSError: pass if hasattr(os, 'getuid'): try: user = os.getuid() if user: return str(user) except OSError: pass user = os.environ.get("USERNAME") if user: return str(user) return None def default_groups(self, request_resources=False): groups = {} groups['api'] = { '': { 'v1': (ResourceGroup( True, resources=self.get_resources_for_api_version('api', '', 'v1', True) ) if request_resources else ResourceGroup(True)) }} if self.version.get('openshift'): groups['oapi'] = { '': { 'v1': (ResourceGroup( True, resources=self.get_resources_for_api_version('oapi', '', 'v1', True) ) if request_resources else ResourceGroup(True)) }} groups[DISCOVERY_PREFIX] = {'': { 'v1': ResourceGroup(True, resources = {"List": [ResourceList(self.client)]}) }} return groups def _load_server_info(self): def just_json(_, serialized): return serialized if not self._cache.get('version'): try: self._cache['version'] = { 'kubernetes': self.client.request('get', '/version', serializer=just_json) } except (ValueError, MaxRetryError) as e: if isinstance(e, MaxRetryError) and not isinstance(e.reason, ProtocolError): raise if not self.client.configuration.host.startswith("https://"): raise ValueError("Host value %s should start with https:// when talking to HTTPS endpoint" % self.client.configuration.host) else: raise try: self._cache['version']['openshift'] = self.client.request( 'get', '/version/openshift', serializer=just_json, ) except ApiException: pass self.__version = self._cache['version'] def get_resources_for_api_version(self, prefix, group, version, preferred): """ returns a dictionary of resources associated with provided (prefix, group, version)""" resources = defaultdict(list) subresources = {} path = '/'.join(filter(None, [prefix, group, version])) try: resources_response = self.client.request('GET', path).resources or [] except ServiceUnavailableError: resources_response = [] resources_raw = list(filter(lambda resource: '/' not in resource['name'], resources_response)) subresources_raw = list(filter(lambda resource: '/' in resource['name'], resources_response)) for subresource in subresources_raw: resource, name = subresource['name'].split('/', 1) if not subresources.get(resource): subresources[resource] = {} subresources[resource][name] = subresource for resource in resources_raw: # Prevent duplicate keys for key in ('prefix', 'group', 'api_version', 'client', 'preferred'): resource.pop(key, None) resourceobj = Resource( prefix=prefix, group=group, api_version=version, client=self.client, preferred=preferred, subresources=subresources.get(resource['name']), **resource ) resources[resource['kind']].append(resourceobj) resource_lookup = { 'prefix': prefix, 'group': group, 'api_version': version, 'kind': resourceobj.kind, 'name': resourceobj.name } resource_list = ResourceList(self.client, group=group, api_version=version, base_kind=resource['kind'], base_resource_lookup=resource_lookup) resources[resource_list.kind].append(resource_list) return resources def get(self, **kwargs): """ Same as search, but will throw an error if there are multiple or no results. If there are multiple results and only one is an exact match on api_version, that resource will be returned. """ results = self.search(**kwargs) # If there are multiple matches, prefer exact matches on api_version if len(results) > 1 and kwargs.get('api_version'): results = [ result for result in results if result.group_version == kwargs['api_version'] ] # If there are multiple matches, prefer non-List kinds if len(results) > 1 and not all([isinstance(x, ResourceList) for x in results]): results = [result for result in results if not isinstance(result, ResourceList)] # if multiple resources are found that share a GVK, prefer the one with the most supported verbs if len(results) > 1 and len(set((x.group_version, x.kind) for x in results)) == 1: if len(set(len(x.verbs) for x in results)) != 1: results = [max(results, key=lambda x: len(x.verbs))] if len(results) == 1: return results[0] elif not results: raise ResourceNotFoundError('No matches found for {}'.format(kwargs)) else: raise ResourceNotUniqueError('Multiple matches found for {}: {}'.format(kwargs, results)) class LazyDiscoverer(K8sLazyDiscoverer, Discoverer): """ A convenient container for storing discovered API resources. Allows easy searching and retrieval of specific resources. Resources for the cluster are loaded lazily. """ class EagerDiscoverer(K8sEagerDiscoverer, Discoverer): """ A convenient container for storing discovered API resources. Allows easy searching and retrieval of specific resources. All resources are discovered for the cluster upon object instantiation. """ openshift-restclient-python-0.13.2/openshift/dynamic/exceptions.py000066400000000000000000000001711446004355700254520ustar00rootroot00000000000000from kubernetes.dynamic.exceptions import * # noqa class ApplyException(Exception): """ Could not apply patch """ openshift-restclient-python-0.13.2/openshift/dynamic/resource.py000066400000000000000000000043631446004355700251270ustar00rootroot00000000000000from kubernetes.dynamic.resource import Resource, Subresource, ResourceField # noqa class ResourceList(Resource): """ Represents a list of API objects """ def __init__(self, client, group='', api_version='v1', base_kind='', kind=None, base_resource_lookup=None): self.client = client self.group = group self.api_version = api_version self.kind = kind or '{}List'.format(base_kind) self.base_kind = base_kind self.base_resource_lookup = base_resource_lookup self.__base_resource = None def base_resource(self): if self.__base_resource: return self.__base_resource elif self.base_resource_lookup: self.__base_resource = self.client.resources.get(**self.base_resource_lookup) return self.__base_resource return None def apply(self, *args, **kwargs): return self.verb_mapper('apply', *args, **kwargs) def to_dict(self): return { '_type': 'ResourceList', 'group': self.group, 'api_version': self.api_version, 'kind': self.kind, 'base_kind': self.base_kind, 'base_resource_lookup': self.base_resource_lookup } def __getattr__(self, name): if self.base_resource(): return getattr(self.base_resource(), name) return None class ResourceInstance(object): """ A parsed instance of an API resource. It exists solely to ease interaction with API objects by allowing attributes to be accessed with '.' notation. """ def __init__(self, client, instance): self.client = client # If we have a list of resources, then set the apiVersion and kind of # each resource in 'items' kind = instance['kind'] if kind.endswith('List') and 'items' in instance: kind = instance['kind'][:-4] if instance['items']: for item in instance['items']: if 'apiVersion' not in item: item['apiVersion'] = instance['apiVersion'] if 'kind' not in item: item['kind'] = kind self.attributes = self.__deserialize(instance) self.__initialised = True openshift-restclient-python-0.13.2/openshift/helper/000077500000000000000000000000001446004355700225535ustar00rootroot00000000000000openshift-restclient-python-0.13.2/openshift/helper/__init__.py000066400000000000000000000000001446004355700246520ustar00rootroot00000000000000openshift-restclient-python-0.13.2/openshift/helper/hashes.py000066400000000000000000000025721446004355700244060ustar00rootroot00000000000000# Implement ConfigMapHash and SecretHash equivalents # Based on https://github.com/kubernetes/kubernetes/pull/49961 import json import hashlib try: import string maketrans = string.maketrans except AttributeError: maketrans = str.maketrans try: from collections import OrderedDict except ImportError: from orderreddict import OrderedDict def sorted_dict(unsorted_dict): result = OrderedDict() for (k, v) in sorted(unsorted_dict.items()): if isinstance(v, dict): v = sorted_dict(v) result[k] = v return result def generate_hash(resource): # Get name from metadata resource['name'] = resource.get('metadata', {}).get('name', '') if resource['kind'] == 'ConfigMap': marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name']) del resource['name'] return encode(marshalled) if resource['kind'] == 'Secret': marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name', 'type']) del resource['name'] return encode(marshalled) raise NotImplementedError def marshal(data, keys): ordered = OrderedDict() for key in keys: ordered[key] = data.get(key, "") return json.dumps(ordered, separators=(',', ':')).encode('utf-8') def encode(resource): return hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt")) openshift-restclient-python-0.13.2/openshift/helper/userpassauth.py000066400000000000000000000131501446004355700256540ustar00rootroot00000000000000# OpenShift User-Password Login helper module # This module extends the `kubernetes.client.Configuration` object. # `OCPLoginConfiguration` uses `username` and `password` params to authenticate # with the OAuth OpenShift component, after the autentication the `api_key` value is set # with the Bearer token. # # IMPORTANT: the Bearer token is designed to expire, si up to the user to renew the token. # the valitity (in secods) is saved into the `token['expires_in`]` attribute. # The value refers to when the token has been created and to not change overtime. # Related discussion on GitHub https://github.com/openshift/openshift-restclient-python/issues/249 # Most part of the code has been taken from the k8s_auth ansible module: # https://github.com/ansible/ansible/blob/stable-2.9/lib/ansible/modules/clustering/k8s/k8s_auth.py import requests from requests_oauthlib import OAuth2Session from urllib3.util import make_headers from urllib.parse import parse_qs, urlencode, urlparse from kubernetes.client import Configuration as KubeConfig class OCPLoginException(Exception): """The base class for the OCPLogin exceptions""" class OCPLoginRequestException(OCPLoginException): def __init__(self, msg, **kwargs): self.msg = msg self.req_info = {} for k, v in kwargs.items(): self.req_info['req_' + k] = v def __str__(self): error_msg = self.msg for k, v in self.req_info.items(): error_msg += '\t{0}: {1}\n'.format(k, v) return error_msg class OCPLoginConfiguration(KubeConfig): def __init__(self, host="http://localhost", api_key=None, api_key_prefix=None, ocp_username=None, ocp_password=None, discard_unknown_keys=False, ): self.ocp_username = ocp_username self.ocp_password = ocp_password super(OCPLoginConfiguration, self).__init__(host=host, api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=discard_unknown_keys) def get_token(self): # python-requests takes either a bool or a path to a ca file as the 'verify' param if self.verify_ssl and self.ssl_ca_cert: self.con_verify_ca = self.ssl_ca_cert # path else: self.con_verify_ca = self.verify_ssl # bool self.discover() self.token = self.login() self.api_key = {"authorization": "Bearer " + self.token['access_token']} self.api_key_expires = self.token['expires_in'] self.api_key_scope = self.token['scope'] def discover(self): url = '{0}/.well-known/oauth-authorization-server'.format(self.host) ret = requests.get(url, verify=self.con_verify_ca) if ret.status_code != 200: raise OCPLoginRequestException("Couldn't find OpenShift's OAuth API", method='GET', url=url, reason=ret.reason, status_code=ret.status_code) oauth_info = ret.json() self.openshift_auth_endpoint = oauth_info['authorization_endpoint'] self.openshift_token_endpoint = oauth_info['token_endpoint'] def login(self): os_oauth = OAuth2Session(client_id='openshift-challenging-client') authorization_url, state = os_oauth.authorization_url(self.openshift_auth_endpoint, state="1", code_challenge_method='S256') auth_headers = make_headers(basic_auth='{0}:{1}'.format(self.ocp_username, self.ocp_password)) # Request authorization code using basic auth credentials ret = os_oauth.get( authorization_url, headers={'X-Csrf-Token': state, 'authorization': auth_headers.get('authorization')}, verify=self.con_verify_ca, allow_redirects=False ) if ret.status_code != 302: raise OCPLoginRequestException("Authorization failed.", method='GET', url=authorization_url, reason=ret.reason, status_code=ret.status_code) qwargs = {} for k, v in parse_qs(urlparse(ret.headers['Location']).query).items(): qwargs[k] = v[0] qwargs['grant_type'] = 'authorization_code' # Using authorization code given to us in the Location header of the previous request, request a token ret = os_oauth.post( self.openshift_token_endpoint, headers={ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', # This is just base64 encoded 'openshift-challenging-client:' 'Authorization': 'Basic b3BlbnNoaWZ0LWNoYWxsZW5naW5nLWNsaWVudDo=' }, data=urlencode(qwargs), verify=self.con_verify_ca ) if ret.status_code != 200: raise OCPLoginRequestException("Failed to obtain an authorization token.", method='POST', url=self.openshift_token_endpoint, reason=ret.reason, status_code=ret.status_code) return ret.json() def logout(self): url = '{0}/apis/oauth.openshift.io/v1/oauthaccesstokens/{1}'.format(self.host, self.api_key) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(self.api_key) } json = { "apiVersion": "oauth.openshift.io/v1", "kind": "DeleteOptions" } requests.delete(url, headers=headers, json=json, verify=self.con_verify_ca) # Ignore errors, the token will time out eventually anyway openshift-restclient-python-0.13.2/python-openshift.spec000066400000000000000000000240051446004355700234700ustar00rootroot00000000000000%if 0%{?rhel} == 7 %bcond_with python3 %bcond_without python2 %else %bcond_with python2 %bcond_without python3 %endif %global library openshift %if 0%{?rhel} == 7 %global py3 python%{python3_pkgversion} %else %global py3 python3 %endif Name: python-%{library} Version: 0.13.2 Release: 1%{?dist} Summary: Python client for the OpenShift API License: ASL 2.0 URL: https://github.com/openshift/openshift-restclient-python Source0: https://github.com/openshift/openshift-restclient-python/archive/v%{version}.tar.gz BuildArch: noarch Epoch: 1 %if 0%{?with_python2} %package -n python2-%{library} Summary: Python client for the OpenShift API %{?python_provide:%python_provide python2-%{library}} BuildRequires: python2-devel BuildRequires: python-setuptools BuildRequires: git Requires: python2 Requires: python2-kubernetes Requires: python2-string_utils Requires: python-requests Requires: python2-ruamel-yaml Requires: python-six Requires: python-jinja2 %description -n python2-%{library} Python client for the kubernetes API. %endif %if 0%{?with_python3} %package -n %{py3}-%{library} Summary: Python client for the OpenShift API BuildRequires: %{py3}-devel BuildRequires: %{py3}-setuptools BuildRequires: git Requires: %{py3} Requires: %{py3}-kubernetes Requires: %{py3}-string_utils Requires: %{py3}-requests Requires: %{py3}-ruamel-yaml Requires: %{py3}-six Requires: %{py3}-jinja2 %description -n %{py3}-%{library} Python client for the OpenShift API %endif # with_python3 #recommonmark not available for docs in EPEL %if 0%{?fedora} %package doc Summary: Documentation for %{name}. Provides: %{name}-doc %if 0%{?with_python3} BuildRequires: %{py3}-sphinx BuildRequires: %{py3}-recommonmark %else BuildRequires: python2-sphinx BuildRequires: python2-recommonmark %endif %description doc %{summary} %endif %description Python client for the OpenShift API %prep %autosetup -n openshift-restclient-python-%{version} -S git #there is no include in RHEL7 setuptools find_packages #the requirements are also done in an non-backwards compatible way %if 0%{?rhel} sed -i -e "s/find_packages(include='openshift.*')/['openshift', 'openshift.dynamic', 'openshift.helper']/g" setup.py sed -i -e '30s/^/REQUIRES = [\n "jinja2",\n "kubernetes",\n "setuptools",\n "six",\n "ruamel.yaml",\n "python-string-utils",\n]\n/g' setup.py sed -i -e "s/extract_requirements('requirements.txt')/REQUIRES/g" setup.py #sed -i -e '14,21d' setup.py %endif %build %if 0%{?with_python2} %py2_build %endif %if 0%{?with_python3} %py3_build %endif %if 0%{?fedora} sphinx-build doc/source/ html %{__rm} -rf html/.buildinfo %endif %install %if 0%{?with_python2} %py2_install %endif %if 0%{?with_python3} %py3_install %endif %check #test dependencies are unpackaged %if 0%{?with_python2} %files -n python2-%{library} %license LICENSE %{python2_sitelib}/%{library}/* %{python2_sitelib}/%{library}-*.egg-info %exclude %{python2_sitelib}/scripts %exclude /usr/requirements.txt/requirements.txt %{_bindir}/openshift-ansible-gen %endif %if 0%{?with_python3} %files -n %{py3}-%{library} %license LICENSE %{python3_sitelib}/%{library}/* %{python3_sitelib}/%{library}-*.egg-info %exclude %{python3_sitelib}/scripts %exclude /usr/requirements.txt/requirements.txt %{_bindir}/openshift-ansible-gen %endif %if 0%{?fedora} %files doc %license LICENSE %doc html %endif %changelog * Tue Dec 4 2018 Jason Montleon 0.9.0-1 - Bump Version to 0.9.0 - Disable python 2 and enable python 3 builds for Fedora * Tue Nov 06 2018 Jason Montleon 0.8.0-1 - Fix tag condition (fabian@fabianism.us) - Add watch to dynamic client (#221) (fabian@fabianism.us) - Pin flake8 (fabian@fabianism.us) - Do not decode response data in Python2 (#225) (16732494+wallecan@users.noreply.github.com) - ResourceContainer does not contain delete method (#227) (mosonkonrad@gmail.com) - Add basic documentation for dynamic client verbs to README (#222) (fabian@fabianism.us) - Add support for *List kinds (#213) (fabian@fabianism.us) - Add validate helper function (#199) (will@thames.id.au) - DynamicApiError: add a summary method (#211) (pierre-louis@libregerbil.fr) - Allow less strict kubernetes version requirements (#207) (will@thames.id.au) - Add behavior-based tests for dynamic client (#208) (fabian@fabianism.us) - Provide 'append_hash' for ConfigMaps and Secrets (#196) (will@thames.id.au) - Allow creates on subresources properly (#201) (fabian@fabianism.us) - Rename async to async_req for compatibility with python3 and kubernetes 7 (#197) (fabian@fabianism.us) - Update kube_config to support concurrent clusters (#193) (tdecacqu@redhat.com) * Mon Aug 06 2018 David Zager 0.6.2-12 - Fix decode issue (#192) (lostonamountain@gmail.com) - b64encode expects bytes not string (fridolin@redhat.com) - Update releasers for 3.11 (david.j.zager@gmail.com) * Mon Jul 23 2018 David Zager 0.6.2-11 - include version update script (fabian@fabianism.us) - Version bump to 0.6.2 (fabian@fabianism.us) * Thu Jul 05 2018 David Zager 0.6.1-10 - Install openshift.dynamic in RPM (#180) (dzager@redhat.com) * Thu Jul 05 2018 David Zager 0.6.1-9 - Call functions on resource fields if they don't exist as name (#179) (will@thames.id.au) - Release 0.6.1 (fabian@fabianism.us) - Fix typo in argument passing for patch in dynamic client. (#176) (fabian@fabianism.us) - Prevent duplicate keys when creating resource (#178) (dzager@redhat.com) - Allow content type specification in resource.patch (#174) (will@thames.id.au) - release 0.6.0 (fabian@fabianism.us) - Default singular name to name sans last letter (#173) (fabian@fabianism.us) - Serialize body more thoroughly, won't always be passed as kwarg (#172) (fabian@fabianism.us) - decode response data for python3 compatibility (#171) (fabian@fabianism.us) - add dynamic client (#167) (fabian@fabianism.us) - Fixes a bug when running fix_serialization on Kubernetes ExternalName… (#161) (zapur1@users.noreply.github.com) * Tue Feb 27 2018 David Zager 0.5.0-8 - Bug 1546843- RuntimeRawExtension objects will now deserialize (fabian@fabianism.us) - Add compatiblity matrix (fabian@fabianism.us) * Thu Feb 22 2018 David Zager 0.5.0-7 - Update client for release k8s-client 5.0 (david.j.zager@gmail.com) - Lint fix (chousekn@redhat.com) - Add 'Bearer' to auth header (chousekn@redhat.com) - All objects will now be instantiated with the proper configuration (fabian@fabianism.us) - Restore API and model matching (chousekn@redhat.com) * Thu Feb 08 2018 David Zager 0.5.0.a1-6 - Allow beta k8s client (david.j.zager@gmail.com) - Update client to use k8s client 5 (david.j.zager@gmail.com) * Fri Jan 19 2018 David Zager 0.4.0.a1-5 - Add object to primitives, treat as string for now (fabian@fabianism.us) - update version to match new scheme (fabian@fabianism.us) - regen modules (fabian@fabianism.us) - Don't exclude modules that appear in both k8s and openshift from codegen (fabian@fabianism.us) - Prefer openshift models to kubernetes models (fabian@fabianism.us) - extra escape characters (fabian@fabianism.us) - Update deployment condition to enforce python versioning standards (fabian@fabianism.us) - Update releasers (david.j.zager@gmail.com) * Tue Jan 16 2018 David Zager 0.4.0-4 - fix linting (fabian@fabianism.us) - Fix ansible module generation for 1.8/3.8 (fabian@fabianism.us) - Remove old OpenShift versions (david.j.zager@gmail.com) - Update watch test (fabian@fabianism.us) - fix a few nil value errors (fabian@fabianism.us) - regen modules (fabian@fabianism.us) - Fixed some errors around object instantiation in the helpers (fabian@fabianism.us) - Generated code (david.j.zager@gmail.com) - Essentials for updating client-python to 4.0 (david.j.zager@gmail.com) - Helper base cleanup (#132) (chousekn@redhat.com) * Mon Dec 04 2017 Jason Montleon 0.3.4-3 - prefix test names with the cluster type (openshift/k8s) to prevent collision (fabian@fabianism.us) - after the argspec is fully created, go through all aliases and remove any collisions (fabian@fabianism.us) - Add test for build config (fabian@fabianism.us) - Update _from conversion to handle all python keywords (fabian@fabianism.us) - Handle _from -> from and vice versa in ansible helper (fabian@fabianism.us) - add exclude for new file that won't be packaged (#125) (jmontleo@redhat.com) - Fix k8s_v1beta1_role_binding 404s (#122) (fabian@fabianism.us) - Pin pytest version due to broken internal API (fabian@fabianism.us) - Add custom_objects_spec.json to package data (ceridwen@users.noreply.github.com) * Fri Nov 03 2017 Jason Montleon 0.3.4-2 - Update version * Fri Nov 03 2017 Jason Montleon 0.3.3-8 - Bug 1508969 - Add foreground propagation policy (david.j.zager@gmail.com) - Document how to use the Dockerfile (david.j.zager@gmail.com) - Add Dockerfile (david.j.zager@gmail.com) - add unit test for watch (fabian@fabianism.us) - Bump version (fabian@fabianism.us) - Support watching openshift resources (fabian@fabianism.us) * Fri Oct 13 2017 Jason Montleon 0.3.3-7 - add python-requests rpm dep * Fri Oct 13 2017 Jason Montleon 0.3.3-6 - Fix module Python interpreter (chousekn@redhat.com) - Version bump (fabian@fabianism.us) - fix version regex and api_version formatting to prevent filtering out valid APIs (fabian@fabianism.us) * Fri Oct 06 2017 Jason Montleon 0.3.2-5 - ignore requirements.txt in packaging * Fri Oct 06 2017 Jason Montleon 0.3.2-4 - * Fri Oct 06 2017 Jason Montleon 0.3.2-3 - make source name match package name * Fri Oct 06 2017 Jason Montleon 0.3.2-2 - Fix source name * Fri Oct 06 2017 Jason Montleon 0.3.2-1 - new package built with tito * Wed May 10 2017 Jason Montleon 1.0.0-0.3 - Initial Build openshift-restclient-python-0.13.2/requirements.txt000066400000000000000000000000551446004355700225610ustar00rootroot00000000000000kubernetes ~= 21.7.0 python-string-utils six openshift-restclient-python-0.13.2/scripts/000077500000000000000000000000001446004355700207645ustar00rootroot00000000000000openshift-restclient-python-0.13.2/scripts/.gitignore000066400000000000000000000000051446004355700227470ustar00rootroot00000000000000.py/ openshift-restclient-python-0.13.2/scripts/__init__.py000066400000000000000000000000001446004355700230630ustar00rootroot00000000000000openshift-restclient-python-0.13.2/scripts/constants.py000066400000000000000000000027071446004355700233600ustar00rootroot00000000000000# Copyright 2016 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys # Kubernetes branch to get the OpenAPI spec from. KUBERNETES_BRANCH = "master" # Spec version will be set in downloaded spec and all # generated code will refer to it. SPEC_VERSION = "v3.9.0" # client version for packaging and releasing. It can # be different than SPEC_VERSION. CLIENT_VERSION = "0.13.2" KUBERNETES_CLIENT_VERSION = "21.7.0" # Name of the release package PACKAGE_NAME = "openshift" # Stage of development, mainly used in setup.py's classifiers. DEVELOPMENT_STATUS = "3 - Alpha" # If called directly, return the constant value given # its name. Useful in bash scripts. if __name__ == '__main__': if len(sys.argv) != 2: print("Usage: python constant.py CONSTANT_NAME") sys.exit(1) if sys.argv[1] in globals(): print(globals()[sys.argv[1]]) else: print("Cannot find constant %s" % sys.argv[1]) sys.exit(1) openshift-restclient-python-0.13.2/scripts/update-version.sh000077500000000000000000000020361446004355700242710ustar00rootroot00000000000000set -x SCRIPT_ROOT=$(dirname "${BASH_SOURCE}") PACKAGE_NAME=$(python "${SCRIPT_ROOT}/constants.py" PACKAGE_NAME) SOURCE_ROOT="${SCRIPT_ROOT}/../" CLIENT_ROOT="${SOURCE_ROOT}/${PACKAGE_NAME}" CLIENT_VERSION=$(python "${SCRIPT_ROOT}/constants.py" CLIENT_VERSION) KUBERNETES_CLIENT_VERSION=$(python "${SCRIPT_ROOT}/constants.py" KUBERNETES_CLIENT_VERSION) echo "--- updating version information..." sed -i'' "s/^CLIENT_VERSION = .*/CLIENT_VERSION = \\\"${CLIENT_VERSION}\\\"/" "${SCRIPT_ROOT}/../setup.py" sed -i'' "s/^__version__ = .*/__version__ = \\\"${CLIENT_VERSION}\\\"/" "${CLIENT_ROOT}/__init__.py" sed -i'' "s/^Version:.*/Version: ${CLIENT_VERSION}/" "${SCRIPT_ROOT}/../python-openshift.spec" sed -i'' "s/^kubernetes ~= .*/kubernetes ~= ${KUBERNETES_CLIENT_VERSION}/" "${SCRIPT_ROOT}/../requirements.txt" sed -i'' "s/^__k8s_client_version__ = .*/__k8s_client_version__ = \\\"${KUBERNETES_CLIENT_VERSION}\\\"/" "${CLIENT_ROOT}/__init__.py" sed -i'' "s/^kubernetes .=.*/kubernetes ~= ${KUBERNETES_CLIENT_VERSION}/" ${SOURCE_ROOT}/requirements.txt openshift-restclient-python-0.13.2/setup.cfg000066400000000000000000000043671446004355700211300ustar00rootroot00000000000000[build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html [yapf] based_on_style = pep8 dedent_closing_brackets = True [tool:pytest] norecursedirs = .* __pycache__ cover docs addopts = --cov=openshift --cov-report=html --cov-append [flake8] # These are things that the devs don't agree make the code more readable # https://github.com/ansible/proposals/issues/50 # E123 closing bracket does not match indentation of opening bracket's line # E124 closing bracket does not match visual indentation # E127 continuation line over-indented for visual indent # E128 continuation line under-indented for visual indent # E201 whitespace after '[' # E202 whitespace before ']' # E203 whitespace before ',' # E221 multiple spaces before operator # E222 multiple spaces after operator # E225 missing whitespace around operator # E226 missing whitespace around arithmetic operator # E227 missing whitespace around bitwise or shift operator # E228 missing whitespace around modulo operator # E231 missing whitespace after ',' # E241 multiple spaces after ',' # E251 unexpected spaces around keyword / parameter equals # E261 at least two spaces before inline comment # E262 inline comment should start with '# ' # E265 block comment should start with '# ' # E266 too many leading '#' for block comment # E301 expected 1 blank line, found 0 # E302 expected 2 blank lines, found 1 # E303 too many blank lines (3) # E402 module level import not at top of file # E502 the backslash is redundant between brackets # E713 test for membership should be 'not in' # E731 do not assign a lambda expression, use a def # W391 blank line at end of file # W503 line break before binary operator # The following matches github.com/ansible/ansible/tox.ini ignore = E123,E124,E127,E128,E201,E202,E203,E211,E221,E222,E225,E226,E228,E227,E231,E241,E251,E261,E262,E265,E266,E301,E302,E303,E402,E502,E713,E731,W391,W503 # Matches line length set at github.com/ansible/ansible/tox.ini max-line-length = 160 # Not worrying about style in the following exclude = .tox/*,test/*,openshift/test/*,openshift/client/models/*,openshift/client/apis/*,openshift/client/__init__.py,openshift/__init__.py,venv*/*,_modules/*,build/*,scripts/from_gen/* openshift-restclient-python-0.13.2/setup.py000066400000000000000000000043271446004355700210150ustar00rootroot00000000000000# Copyright 2016 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ To install the library, run the following python setup.py install prerequisite: setuptools http://pypi.python.org/pypi/setuptools """ from setuptools import find_packages, setup # Do not edit these constants. They will be updated automatically # by scripts/update-client.sh. CLIENT_VERSION = "0.13.2" PACKAGE_NAME = "openshift" DEVELOPMENT_STATUS = "3 - Alpha" def extract_requirements(filename): """ Extracts requirements from a pip formatted requirements file. """ with open(filename, 'r') as requirements_file: return requirements_file.read().splitlines() setup( name=PACKAGE_NAME, version=CLIENT_VERSION, description="OpenShift python client", author_email="", author="OpenShift", license="Apache License Version 2.0", url="https://github.com/openshift/openshift-restclient-python", keywords=["Swagger", "OpenAPI", "Kubernetes", "OpenShift"], install_requires=['kubernetes >= 12.0', 'python-string-utils', 'six'], packages=find_packages(include='openshift.*'), long_description='Python client for OpenShift http://openshift.redhat.com/', classifiers=[ "Development Status :: %s" % DEVELOPMENT_STATUS, "Topic :: Utilities", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "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", ], ) openshift-restclient-python-0.13.2/test-requirements.txt000066400000000000000000000000421446004355700235320ustar00rootroot00000000000000coverage flake8 pytest pytest-cov openshift-restclient-python-0.13.2/test/000077500000000000000000000000001446004355700202545ustar00rootroot00000000000000openshift-restclient-python-0.13.2/test/Dockerfile000066400000000000000000000013271446004355700222510ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi8/ubi ENV USER_UID=1001 \ USER_NAME=openshift-python\ HOME=/opt/osrcp RUN yum install -y \ glibc-langpack-en \ git \ make \ python3 \ python3-devel \ python3-pip \ python3-setuptools \ && pip3 install --no-cache-dir --upgrade setuptools pip wheel \ && yum clean all COPY . /opt/osrcp WORKDIR /opt/osrcp RUN pip install -e . && \ pip install -r test-requirements.txt RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd \ && chown -R "${USER_UID}:0" "${HOME}" \ && chmod -R ug+rwX "${HOME}" \ && mkdir /go \ && chown -R "${USER_UID}:0" /go \ && chmod -R ug+rwX /go USER ${USER_UID} openshift-restclient-python-0.13.2/test/integration/000077500000000000000000000000001446004355700225775ustar00rootroot00000000000000openshift-restclient-python-0.13.2/test/integration/__init__.py000066400000000000000000000017111446004355700247100ustar00rootroot00000000000000import unittest from kubernetes import config from kubernetes.client import api_client, Configuration from openshift.dynamic import DynamicClient class RestClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): try: config.load_kube_config() except config.ConfigException: config.load_incluster_config() cls.config = Configuration.get_default_copy() cls.client = DynamicClient(api_client.ApiClient(configuration=cls.config)) def setUp(self): # Generate a namespace name from the test case name self.namespace = self.id().split('.')[-1].replace('_', '-') v1_namespaces = self.client.resources.get(api_version='v1', kind='Namespace') self.addCleanup(v1_namespaces.delete, name=self.namespace) v1_namespaces.create(body=dict( apiVersion='v1', kind='Namespace', metadata=dict(name=self.namespace), )) openshift-restclient-python-0.13.2/test/integration/test_client.py000066400000000000000000000263461446004355700255010ustar00rootroot00000000000000import time import uuid from openshift.dynamic.exceptions import ResourceNotFoundError from . import RestClientTestCase def short_uuid(): id = str(uuid.uuid4()) return id[-12:] class TestDynamicClient(RestClientTestCase): def test_cluster_custom_resources(self): with self.assertRaises(ResourceNotFoundError): changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ClusterChangeMe') crd_api = self.client.resources.get( api_version='apiextensions.k8s.io/v1beta1', kind='CustomResourceDefinition') name = 'clusterchangemes.apps.example.com' crd_manifest = { 'apiVersion': 'apiextensions.k8s.io/v1beta1', 'kind': 'CustomResourceDefinition', 'metadata': { 'name': name, }, 'spec': { 'group': 'apps.example.com', 'names': { 'kind': 'ClusterChangeMe', 'listKind': 'ClusterChangeMeList', 'plural': 'clusterchangemes', 'singular': 'clusterchangeme', }, 'scope': 'Cluster', 'version': 'v1', 'subresources': { 'status': {} } } } resp = crd_api.create(crd_manifest) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) resp = crd_api.get( name=name, ) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) try: changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ClusterChangeMe') except ResourceNotFoundError: # Need to wait a sec for the discovery layer to get updated time.sleep(2) changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ClusterChangeMe') resp = changeme_api.get() self.assertEqual(resp.items, []) changeme_name = 'custom-resource' + short_uuid() changeme_manifest = { 'apiVersion': 'apps.example.com/v1', 'kind': 'ClusterChangeMe', 'metadata': { 'name': changeme_name, }, 'spec': {} } resp = changeme_api.create(body=changeme_manifest) self.assertEqual(resp.metadata.name, changeme_name) resp = changeme_api.get(name=changeme_name) self.assertEqual(resp.metadata.name, changeme_name) changeme_manifest['spec']['size'] = 3 resp = changeme_api.patch( body=changeme_manifest, content_type='application/merge-patch+json' ) self.assertEqual(resp.spec.size, 3) resp = changeme_api.get(name=changeme_name) self.assertEqual(resp.spec.size, 3) resp = changeme_api.get() self.assertEqual(len(resp.items), 1) resp = changeme_api.delete( name=changeme_name, ) resp = changeme_api.get() self.assertEqual(len(resp.items), 0) resp = crd_api.delete( name=name, ) time.sleep(2) self.client.resources.invalidate_cache() with self.assertRaises(ResourceNotFoundError): changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ClusterChangeMe') def test_namespaced_custom_resources(self): with self.assertRaises(ResourceNotFoundError): changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ChangeMe') crd_api = self.client.resources.get( api_version='apiextensions.k8s.io/v1beta1', kind='CustomResourceDefinition') name = 'changemes.apps.example.com' crd_manifest = { 'apiVersion': 'apiextensions.k8s.io/v1beta1', 'kind': 'CustomResourceDefinition', 'metadata': { 'name': name, }, 'spec': { 'group': 'apps.example.com', 'names': { 'kind': 'ChangeMe', 'listKind': 'ChangeMeList', 'plural': 'changemes', 'singular': 'changeme', }, 'scope': 'Namespaced', 'version': 'v1', 'subresources': { 'status': {} } } } resp = crd_api.create(crd_manifest) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) resp = crd_api.get( name=name, ) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) try: changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ChangeMe') except ResourceNotFoundError: # Need to wait a sec for the discovery layer to get updated time.sleep(2) changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ChangeMe') resp = changeme_api.get() self.assertEqual(resp.items, []) changeme_name = 'custom-resource' + short_uuid() changeme_manifest = { 'apiVersion': 'apps.example.com/v1', 'kind': 'ChangeMe', 'metadata': { 'name': changeme_name, }, 'spec': {} } resp = changeme_api.create(body=changeme_manifest, namespace=self.namespace) self.assertEqual(resp.metadata.name, changeme_name) resp = changeme_api.get(name=changeme_name, namespace=self.namespace) self.assertEqual(resp.metadata.name, changeme_name) changeme_manifest['spec']['size'] = 3 resp = changeme_api.patch( body=changeme_manifest, namespace=self.namespace, content_type='application/merge-patch+json' ) self.assertEqual(resp.spec.size, 3) resp = changeme_api.get(name=changeme_name, namespace=self.namespace) self.assertEqual(resp.spec.size, 3) resp = changeme_api.get(namespace=self.namespace) self.assertEqual(len(resp.items), 1) resp = changeme_api.get() self.assertEqual(len(resp.items), 1) resp = changeme_api.delete( name=changeme_name, namespace=self.namespace ) resp = changeme_api.get(namespace=self.namespace) self.assertEqual(len(resp.items), 0) resp = changeme_api.get() self.assertEqual(len(resp.items), 0) resp = crd_api.delete( name=name, ) time.sleep(2) self.client.resources.invalidate_cache() with self.assertRaises(ResourceNotFoundError): changeme_api = self.client.resources.get( api_version='apps.example.com/v1', kind='ChangeMe') def test_service_apis(self): api = self.client.resources.get(api_version='v1', kind='Service') name = 'frontend-' + short_uuid() 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}}} resp = api.create( body=service_manifest, namespace=self.namespace ) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) resp = api.get( name=name, namespace=self.namespace ) self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) service_manifest['spec']['ports'] = [{'name': 'new', 'port': 8080, 'protocol': 'TCP', 'targetPort': 8080}] resp = api.patch( body=service_manifest, name=name, namespace=self.namespace ) self.assertEqual(2, len(resp.spec.ports)) self.assertTrue(resp.status) resp = api.delete( name=name, body={}, namespace=self.namespace ) def test_replication_controller_apis(self): api = self.client.resources.get( api_version='v1', kind='ReplicationController') name = 'frontend-' + short_uuid() rc_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'}]}]}}}} resp = api.create( body=rc_manifest, namespace=self.namespace) self.assertEqual(name, resp.metadata.name) self.assertEqual(2, resp.spec.replicas) resp = api.get( name=name, namespace=self.namespace) self.assertEqual(name, resp.metadata.name) self.assertEqual(2, resp.spec.replicas) resp = api.delete( name=name, body={}, namespace=self.namespace) def test_configmap_apis(self): api = self.client.resources.get(api_version='v1', kind='ConfigMap') name = 'test-configmap-' + short_uuid() test_configmap = { "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": name, }, "data": { "config.json": "{\"command\":\"/usr/bin/mysqld_safe\"}", "frontend.cnf": "[mysqld]\nbind-address = 10.0.0.3\n" } } resp = api.create( body=test_configmap, namespace=self.namespace ) self.assertEqual(name, resp.metadata.name) resp = api.get( name=name, namespace=self.namespace) self.assertEqual(name, resp.metadata.name) test_configmap['data']['config.json'] = "{}" resp = api.patch( name=name, namespace=self.namespace, body=test_configmap) resp = api.delete( name=name, body={}, namespace=self.namespace) resp = api.get(namespace=self.namespace, pretty=True) self.assertEqual([], resp.items) def test_node_apis(self): api = self.client.resources.get(api_version='v1', kind='Node') for item in api.get().items: node = api.get(name=item.metadata.name) self.assertTrue(len(dict(node.metadata.labels)) > 0) openshift-restclient-python-0.13.2/test/integration/test_openshift_apis.py000066400000000000000000000120611446004355700272230ustar00rootroot00000000000000import time from datetime import datetime import requests from . import RestClientTestCase class TestOpenshiftApis(RestClientTestCase): def create_hello_openshift(self): apps_v1_deployments = self.client.resources.get(api_version='apps/v1', kind='Deployment') v1_services = self.client.resources.get(api_version='v1', kind='Service') name = 'hello-openshift' deployment = { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": name, "namespace": self.namespace }, "spec": { "replicas": 3, "selector": { "matchLabels": { "app": "hello-openshift" } }, "template": { "metadata": { "labels": { "app": "hello-openshift" } }, "spec": { "containers": [{ "name": "hello-openshift", "image": "docker.io/openshift/hello-openshift", "ports": [{"containerPort": 8080}] }] }}}} service = { "apiVersion": "v1", "kind": "Service", "metadata": { "name": name, "namespace": self.namespace }, "spec": { "ports": [{ "port": 80, "targetPort": 8080 }], "selector": { "app": "hello-openshift" } }} self.addCleanup(apps_v1_deployments.delete, name=name, namespace=self.namespace) apps_v1_deployments.create(deployment) self.addCleanup(v1_services.delete, name=name, namespace=self.namespace) v1_services.create(service) # Wait 1 minute for deployment to become available timeout = 60 start = datetime.now() while (datetime.now() - start).seconds < timeout: try: deployment = apps_v1_deployments.get(name=name, namespace=self.namespace) if (deployment.status and deployment.spec.replicas == (deployment.status.replicas or 0) and deployment.status.readyReplicas == deployment.spec.replicas and deployment.status.observedGeneration == deployment.metadata.generation and not deployment.status.unavailableReplicas): return time.sleep(1) except Exception: time.sleep(1) def test_v1_route(self): self.create_hello_openshift() v1_routes = self.client.resources.get(api_version='route.openshift.io/v1', kind='Route') route = { "apiVersion": "route.openshift.io/v1", "kind": "Route", "metadata": { "name": "test-route", "namespace": self.namespace, }, "spec": { "to": { "kind": "Service", "name": "hello-openshift", }, "port": { "targetPort": 8080 }, "tls": { "termination": "Edge", "insecureEdgeTerminationPolicy": "Redirect", } } } self.addCleanup(v1_routes.delete, name='test-route', namespace=self.namespace) created_route = v1_routes.create(route) url = created_route.spec.host timeout = 10 start = datetime.now() while (datetime.now() - start).seconds < timeout: response = requests.get("https://{0}".format(url), verify=False) if response.status_code == 200: break assert response.text == "Hello OpenShift!\n" def test_templates(self): v1_templates = self.client.resources.get(api_version='template.openshift.io/v1', name='templates') v1_processed_templates = self.client.resources.get(api_version='template.openshift.io/v1', name='processedtemplates') nginx_template = v1_templates.get(name='nginx-example', namespace='openshift').to_dict() nginx_template = self.update_template_param(nginx_template, 'NAMESPACE', self.namespace) nginx_template = self.update_template_param(nginx_template, 'NAME', 'test123') response = v1_processed_templates.create(body=nginx_template, namespace=self.namespace) for obj in response.objects: if obj.metadata.namespace: assert obj.metadata.namespace == self.namespace assert obj.metadata.name == 'test123' def update_template_param(self, template, k, v): for i, param in enumerate(template['parameters']): if param['name'] == k: template['parameters'][i]['value'] = v return template return template openshift-restclient-python-0.13.2/test/unit/000077500000000000000000000000001446004355700212335ustar00rootroot00000000000000openshift-restclient-python-0.13.2/test/unit/test_apply.py000066400000000000000000000356241446004355700240030ustar00rootroot00000000000000from openshift.dynamic.apply import merge, apply_patch tests = [ dict( last_applied = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), expected = {} ), dict( last_applied = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2", three="3") ), expected = dict(data=dict(three="3")) ), dict( last_applied = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ), expected = dict(data=dict(two=None, three="3")) ), dict( last_applied = dict( kind="ConfigMap", metadata=dict(name="foo", annotations=dict(this="one", hello="world")), data=dict(one="1", two="2") ), desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ), expected = dict(metadata=dict(annotations=None), data=dict(two=None, three="3")) ), dict( last_applied = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), expected = dict(spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")])) ), dict( last_applied = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected = dict(spec=dict(ports=[dict(port=8081, name="http")])) ), dict( last_applied = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) ), expected = dict(spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http", protocol='TCP')])) ), dict( last_applied = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) ), actual = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, protocol='TCP', name="https"), dict(port=8080, protocol='TCP', name='http')]) ), desired = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), expected = dict(spec=dict(ports=[dict(port=8080, name="http", protocol='TCP')])) ), dict( last_applied = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https", madeup="xyz"), dict(port=8080, name="http")]) ), actual = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, protocol='TCP', name="https", madeup="xyz"), dict(port=8080, protocol='TCP', name='http')]) ), desired = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https")]) ), expected = dict(spec=dict(ports=[dict(madeup=None, port=8443, name="https", protocol='TCP')])) ), dict( last_applied = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) ), actual = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) ), desired = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(memory="50Mi")))]) ), expected=dict(spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(cpu=None, memory="50Mi")))])) ), dict( desired = dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), ])), last_applied = dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), ])), actual = dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test"), dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz")), ])), expected = dict(spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test"), dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz")), ])), ), # This next one is based on a real world case where definition was mostly # str type and everything else was mostly unicode type (don't ask me how) dict( last_applied = { u'kind': u'ConfigMap', u'data': {u'one': '1', 'three': '3', 'two': '2'}, u'apiVersion': u'v1', u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap'} }, actual = { u'kind': u'ConfigMap', u'data': {u'one': '1', 'three': '3', 'two': '2'}, u'apiVersion': u'v1', u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap', u'resourceVersion': '1714994', u'creationTimestamp': u'2019-08-17T05:08:05Z', u'annotations': {}, u'selfLink': u'/api/v1/namespaces/apply/configmaps/apply-configmap', u'uid': u'fed45fb0-c0ac-11e9-9d95-025000000001'} }, desired = { 'kind': u'ConfigMap', 'data': {'one': '1', 'three': '3', 'two': '2'}, 'apiVersion': 'v1', 'metadata': {'namespace': 'apply', 'name': 'apply-configmap'} }, expected = dict() ), # apply a Deployment, then scale the Deployment (which doesn't affect last-applied) # then apply the Deployment again. Should un-scale the Deployment dict( last_applied = { 'kind': u'Deployment', 'spec': { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-xyz' } }, { 'secretRef': { 'name': 'config-wxy' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, actual = { 'kind': u'Deployment', 'spec': { 'replicas': 0, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-xyz' } }, { 'secretRef': { 'name': 'config-wxy' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, desired = { 'kind': u'Deployment', 'spec': { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-abc' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, expected = { 'spec' : { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-abc' } } ] } ] } } } } ), dict( last_applied = { 'kind': 'MadeUp', 'toplevel': { 'original': 'entry' } }, actual = { 'kind': 'MadeUp', 'toplevel': { 'original': 'entry', 'another': { 'nested': { 'entry': 'value' } } } }, desired = { 'kind': 'MadeUp', 'toplevel': { 'original': 'entry', 'another': { 'nested': { 'entry': 'value' } } } }, expected = {} ) ] def test_merges(): for test in tests: assert(merge(test['last_applied'], test['desired'], test.get('actual', test['last_applied'])) == test['expected']) def test_apply_patch(): actual = dict( kind="ConfigMap", metadata=dict(name="foo", annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","two":"2"},"kind":"ConfigMap",' '"metadata":{"annotations":{"hello":"world","this":"one"},"name":"foo"}}', 'this': 'one', 'hello': 'world'}), data=dict(one="1", two="2") ) desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ) expected = dict( metadata=dict( annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","three":"3"},"kind":"ConfigMap","metadata":{"name":"foo"}}', 'this': None, 'hello': None}), data=dict(two=None, three="3")) assert(apply_patch(actual, desired) == (actual, expected)) openshift-restclient-python-0.13.2/test/unit/test_diff.py000066400000000000000000000075261446004355700235660ustar00rootroot00000000000000from openshift.dynamic.apply import recursive_diff tests = [ dict( before = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), after = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), expected = None ), dict( before = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), after = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected = ( dict(spec=dict(ports=[dict(port=8080, name="http")])), dict(spec=dict(ports=[dict(port=8081, name="http")])) ) ), dict( before = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http"), dict(port=8081, name="https")]) ), after = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="https"), dict(port=8080, name="http")]) ), expected = None ), dict( before = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", env=[dict(name="hello", value="world"), dict(name="another", value="next")])]) ), after = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", env=[dict(name="hello", value="everyone")])]) ), expected=(dict(spec=dict(containers=[dict(name="busybox", env=[dict(name="another", value="next"), dict(name="hello", value="world")])])), dict(spec=dict(containers=[dict(name="busybox", env=[dict(name="hello", value="everyone")])]))) ), dict( before = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox")]) ), after = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected=(dict(kind='Pod', spec=dict(containers=[dict(image='busybox', name='busybox')])), dict(kind='Service', spec=dict(ports=[dict(name='http', port=8081)]))) ), dict( before = dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox")]) ), after = dict( # kind="...", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected=(dict(kind='Pod', spec=dict(containers=[dict(image='busybox', name='busybox')])), dict(spec=dict(ports=[dict(name='http', port=8081)]))) ), dict( before = dict( # kind="...", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox")]) ), after = dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected=(dict(spec=dict(containers=[dict(image='busybox', name='busybox')])), dict(kind='Service', spec=dict(ports=[dict(name='http', port=8081)]))) ), ] def test_diff(): for test in tests: assert(recursive_diff(test['before'], test['after']) == test['expected']) openshift-restclient-python-0.13.2/test/unit/test_discoverer.py000066400000000000000000000077601446004355700250230ustar00rootroot00000000000000import pytest from kubernetes.client import ApiClient from openshift.dynamic import ( DynamicClient, Resource, ResourceList, EagerDiscoverer, LazyDiscoverer ) @pytest.fixture(scope='module') def mock_namespace(): return Resource( api_version='v1', kind='Namespace', name='namespaces', namespaced=False, preferred=True, prefix='api', shorter_names=['ns'], shortNames=['ns'], singularName='namespace', verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] ) @pytest.fixture(scope='module') def mock_templates(): return Resource( api_version='v1', kind='Template', name='templates', namespaced=True, preferred=True, prefix='api', shorter_names=[], shortNames=[], verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] ) @pytest.fixture(scope='module') def mock_processedtemplates(): return Resource( api_version='v1', kind='Template', name='processedtemplates', namespaced=True, preferred=True, prefix='api', shorter_names=[], shortNames=[], verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] ) @pytest.fixture(scope='module') def mock_namespace_list(mock_namespace): ret = ResourceList(mock_namespace.client, mock_namespace.group, mock_namespace.api_version, mock_namespace.kind) ret._ResourceList__base_resource = mock_namespace return ret @pytest.fixture(scope='function', autouse=True) def setup_client_monkeypatch(monkeypatch, mock_namespace, mock_namespace_list, mock_templates, mock_processedtemplates): def mock_load_server_info(self): self.__version = {'kubernetes': 'mock-k8s-version'} def mock_parse_api_groups(self, request_resources=False): return { 'api': { '': { 'v1': { 'Namespace': [mock_namespace], 'NamespaceList': [mock_namespace_list], 'Template': [mock_templates, mock_processedtemplates], } } } } monkeypatch.setattr(EagerDiscoverer, '_load_server_info', mock_load_server_info) monkeypatch.setattr(EagerDiscoverer, 'parse_api_groups', mock_parse_api_groups) monkeypatch.setattr(LazyDiscoverer, '_load_server_info', mock_load_server_info) monkeypatch.setattr(LazyDiscoverer, 'parse_api_groups', mock_parse_api_groups) @pytest.fixture(params=[EagerDiscoverer, LazyDiscoverer]) def client(request): return DynamicClient(ApiClient(), discoverer=request.param) @pytest.mark.parametrize(("attribute", "value"), [ ('name', 'namespaces'), ('singular_name', 'namespace'), ('short_names', ['ns']) ]) def test_search_returns_single_and_list(client, mock_namespace, mock_namespace_list, attribute, value): resources = client.resources.search(**{'api_version':'v1', attribute: value}) assert len(resources) == 2 assert mock_namespace in resources assert mock_namespace_list in resources @pytest.mark.parametrize(("attribute", "value"), [ ('kind', 'Namespace'), ('name', 'namespaces'), ('singular_name', 'namespace'), ('short_names', ['ns']) ]) def test_get_returns_only_single(client, mock_namespace, attribute, value): resource = client.resources.get(**{'api_version':'v1', attribute: value}) assert resource == mock_namespace def test_get_namespace_list_kind(client, mock_namespace_list): resource = client.resources.get(api_version='v1', kind='NamespaceList') assert resource == mock_namespace_list def test_search_multiple_resources_for_template(client, mock_templates, mock_processedtemplates): resources = client.resources.search(api_version='v1', kind='Template') assert len(resources) == 2 assert mock_templates in resources assert mock_processedtemplates in resources openshift-restclient-python-0.13.2/test/unit/test_hashes.py000066400000000000000000000030051446004355700241150ustar00rootroot00000000000000# Test ConfigMapHash and SecretHash equivalents # tests based on https://github.com/kubernetes/kubernetes/pull/49961 from openshift.helper.hashes import generate_hash tests = [ dict( resource = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict() ), expected = "867km9574f", ), dict( resource = dict( kind="ConfigMap", metadata=dict(name="foo"), type="my-type", data=dict() ), expected = "867km9574f", ), dict( resource = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict( key1="value1", key2="value2") ), expected = "gcb75dd9gb", ), dict( resource = dict( kind="Secret", metadata=dict(name="foo"), data=dict() ), expected = "949tdgdkgg", ), dict( resource = dict( kind="Secret", metadata=dict(name="foo"), type="my-type", data=dict() ), expected = "dg474f9t76", ), dict( resource = dict( kind="Secret", metadata=dict(name="foo"), data=dict( key1="dmFsdWUx", key2="dmFsdWUy") ), expected = "tf72c228m4", ) ] def test_hashes(): for test in tests: assert(generate_hash(test['resource']) == test['expected']) openshift-restclient-python-0.13.2/test/unit/test_marshal.py000066400000000000000000000036011446004355700242730ustar00rootroot00000000000000# Test ConfigMap and Secret marshalling # tests based on https://github.com/kubernetes/kubernetes/pull/49961 from openshift.helper.hashes import marshal, sorted_dict tests = [ dict( resource=dict( kind="ConfigMap", name="", data=dict(), ), expected=b'{"data":{},"kind":"ConfigMap","name":""}' ), dict( resource=dict( kind="ConfigMap", name="", data=dict( one="" ), ), expected=b'{"data":{"one":""},"kind":"ConfigMap","name":""}' ), dict( resource=dict( kind="ConfigMap", name="", data=dict( two="2", one="", three="3", ), ), expected=b'{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}' ), dict( resource=dict( kind="Secret", type="my-type", name="", data=dict(), ), expected=b'{"data":{},"kind":"Secret","name":"","type":"my-type"}' ), dict( resource=dict( kind="Secret", type="my-type", name="", data=dict( one="" ), ), expected=b'{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}' ), dict( resource=dict( kind="Secret", type="my-type", name="", data=dict( two="Mg==", one="", three="Mw==", ), ), expected=b'{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}' ), ] def test_marshal(): for test in tests: assert(marshal(sorted_dict(test['resource']), sorted(list(test['resource'].keys()))) == test['expected']) openshift-restclient-python-0.13.2/tox.ini000066400000000000000000000042711446004355700206140ustar00rootroot00000000000000[tox] envlist = py310-lint py39-lint py39-openshift-unit [testenv] usedevelop = True deps = -rrequirements.txt -rtest-requirements.txt py39-lint: flake8-bugbear py310-lint: flake8-bugbear whitelist_externals = /bin/bash /bin/git commands = lint: flake8 unit: pytest test/unit -v -r s [travis:env] TEST_SUITE = lint: lint unit: unit [flake8] # These are things that the devs don't agree make the code more readable # https://github.com/ansible/proposals/issues/50 # E123 closing bracket does not match indentation of opening bracket's line # E124 closing bracket does not match visual indentation # E127 continuation line over-indented for visual indent # E128 continuation line under-indented for visual indent # E201 whitespace after '[' # E202 whitespace before ']' # E203 whitespace before ',' # E221 multiple spaces before operator # E222 multiple spaces after operator # E225 missing whitespace around operator # E226 missing whitespace around arithmetic operator # E227 missing whitespace around bitwise or shift operator # E228 missing whitespace around modulo operator # E231 missing whitespace after ',' # E241 multiple spaces after ',' # E251 unexpected spaces around keyword / parameter equals # E261 at least two spaces before inline comment # E262 inline comment should start with '# ' # E265 block comment should start with '# ' # E266 too many leading '#' for block comment # E301 expected 1 blank line, found 0 # E302 expected 2 blank lines, found 1 # E303 too many blank lines (3) # E402 module level import not at top of file # E502 the backslash is redundant between brackets # E713 test for membership should be 'not in' # E731 do not assign a lambda expression, use a def # W391 blank line at end of file # W503 line break before binary operator # The following matches github.com/ansible/ansible/tox.ini ignore = E123,E124,E127,E128,E201,E202,E203,E211,E221,E222,E225,E226,E228,E227,E231,E241,E251,E261,E262,E265,E266,E301,E302,E303,E402,E502,E713,E731,W391,W503 # Matches line length set at github.com/ansible/ansible/tox.ini max-line-length = 160 # Not worrying about style in the following exclude = .tox/*,test/*,venv*/*,_modules/*,build/*,scripts/*