├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.rst ├── kubeshell ├── __init__.py ├── client.py ├── completer.py ├── data │ └── cli.json ├── kubeshell.py ├── lexer.py ├── logger.py ├── main.py ├── parser.py ├── style.py ├── tests │ └── test_cli.py └── toolbar.py ├── misc └── python_eats_cobra.go ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.DS_Store 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | .cache 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | # Emacs backup files 31 | *~ 32 | 33 | # Eclipse IDE 34 | /.project 35 | /.pydevproject 36 | 37 | # IDEA IDE 38 | .idea* 39 | src/ 40 | 41 | # Completions Index 42 | completions.idx 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: python 4 | cache: pip 5 | python: 6 | - "2.7.10" 7 | - "2.7.13" 8 | - "3.5" 9 | - "3.6" 10 | before_install: 11 | - pip install flake8 12 | before_script: 13 | # stop the build if there are Python syntax errors or undefined names 14 | - flake8 . --count --select=E901,E999,F821,F822,F823 --statistics 15 | # exit-zero treates all errors as warnings. The GitHub editor is 127 chars wide 16 | - flake8 . --count --exit-zero --max-line-length=127 --statistics 17 | install: 18 | - python setup.py install 19 | - pip install pexpect 20 | script: 21 | - python kubeshell/tests/test_cli.py 22 | sudo: false 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.21 2 | 3 | - suppressing urllib3 warnings and errors 4 | - client-python library issues due to failed connections 5 | - logging kube-shell errors to `$HOME/.kube/shell/error.log` 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | kube-shell 2 | ============== 3 | 4 | |Build Status| |PyPI version| |PyPI pyversions| |License| |Gitter chat| 5 | 6 | Kube-shell: An integrated shell for working with the Kubernetes CLI 7 | 8 | Under the hood kube-shell still calls kubectl. Kube-shell aims to 9 | provide ease-of-use of kubectl and increasing productivity. 10 | 11 | kube-shell features 12 | ------------------- 13 | 14 | Auto Completion of Commands and Options with in-line documentation 15 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 16 | 17 | .. figure:: http://i.imgur.com/dfelkKr.gif 18 | :alt: 19 | 20 | Fish-Style Auto Suggestions 21 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 22 | 23 | .. figure:: http://i.imgur.com/7VciOuR.png 24 | :alt: 25 | 26 | Command History 27 | ^^^^^^^^^^^^^^^ 28 | 29 | You can use up-arrow and down-arrow to walk through the history of 30 | commands executed. Also up-arrow partial string matching is possible. 31 | For e.g. enter 'kubectl get' and use up-arrow and down-arrow to browse 32 | through all kubectl get commands. You could also use CTRL+r to search 33 | from the history of commands. 34 | 35 | .. figure:: http://i.imgur.com/xsIM3QV.png 36 | :alt: 37 | 38 | Fuzzy Searching 39 | ^^^^^^^^^^^^^^^ 40 | 41 | .. figure:: http://i.imgur.com/tW9oAUO.png 42 | :alt: 43 | 44 | Server Side Auto Completion 45 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 46 | 47 | .. figure:: http://i.imgur.com/RAfHXjx.gif 48 | :alt: 49 | 50 | Context information 51 | ^^^^^^^^^^^^^^^^^^^ 52 | 53 | Details of current context from kubeconfig is always readily displayed 54 | on the bottom toolbar. By pressing F4 button you can switch between the 55 | clusters and using F5 can switch between namespaces. 56 | 57 | .. figure:: http://i.imgur.com/MJLgcj3.png 58 | :alt: 59 | 60 | vi editing mode 61 | ^^^^^^^^^^^^^^^ 62 | 63 | Press ESC you have all key bindings (w: next word, b: prev word) to move 64 | across the words. 65 | 66 | Installation 67 | ------------ 68 | 69 | The kube-shell requires python and 70 | `pip `__ to install. You can 71 | install the kube-shell using ``pip``: 72 | 73 | .. code:: bash 74 | 75 | $ pip install kube-shell 76 | 77 | Usage 78 | ----- 79 | 80 | After installing kube-shell through pip, just run kube-shell to bring up 81 | shell. 82 | 83 | At the kube-shell command prompt you can run exit or press F10 to exit 84 | the shell. You can clear the screen by running clear command. 85 | 86 | By default drop-down suggestion list also displays in-line 87 | documentation, you can turn on/off inline documnetation by pressing F4 88 | button. 89 | 90 | You can run any shell command by prefixing command with "!". For e.g. 91 | !ls would list from the current directory. 92 | 93 | Under the hood 94 | -------------- 95 | 96 | Other than generation of suggestions all heavy lifting is done by 97 | Python's `prompt 98 | toolkit `__, 99 | `Pygments `__ libraries. 100 | 101 | A GO `program `__ is used to generate 102 | kubectl's commands, subcommands, arguments, global options and local 103 | options in `json `__ format. Kube-shell uses 104 | generated json file to suggest commands, subcommands, options and args. 105 | For server side completion kube-shell uses 106 | `client-python `__ 107 | libray to fetch the resources. 108 | 109 | Status 110 | ------ 111 | 112 | Kube-shell should be useful now. But given that its aim is to increase 113 | productivity and easy of use, it can be improved in a number of ways. If 114 | you have suggestions for improvements or new features, or run into a bug 115 | please open an issue 116 | `here `__ or chat 117 | in the `Gitter `__. 118 | 119 | Acknowledgement 120 | --------------- 121 | 122 | Kube-shell is inspired by `AWS 123 | Shell `__, 124 | `SAWS `__ and uses awesome Python 125 | `prompt 126 | toolkit `__ 127 | 128 | .. |Build Status| image:: https://travis-ci.org/cloudnativelabs/kube-shell.svg?branch=master 129 | :target: https://travis-ci.org/cloudnativelabs/kube-shell 130 | .. |PyPI version| image:: https://badge.fury.io/py/kube-shell.svg 131 | :target: https://badge.fury.io/py/kube-shell 132 | .. |PyPI pyversions| image:: https://img.shields.io/pypi/pyversions/ansicolortags.svg 133 | :target: https://pypi.python.org/pypi/kube-shell/ 134 | .. |License| image:: http://img.shields.io/:license-apache-blue.svg 135 | :target: http://www.apache.org/licenses/LICENSE-2.0.html 136 | .. |Gitter chat| image:: http://badges.gitter.im/kube-shell/Lobby.svg 137 | :target: https://gitter.im/kube-shell/Lobby 138 | -------------------------------------------------------------------------------- /kubeshell/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.23' 2 | from . import logger 3 | -------------------------------------------------------------------------------- /kubeshell/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function 2 | from urllib3.exceptions import NewConnectionError, ConnectTimeoutError, MaxRetryError 3 | from kubernetes import client, config 4 | from kubernetes.client.api_client import ApiException 5 | 6 | import os 7 | import logging 8 | import urllib3 9 | 10 | # disable warnings on stdout/stderr from urllib3 connection errors 11 | ulogger = logging.getLogger("urllib3") 12 | ulogger.setLevel("ERROR") 13 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 14 | kubeconfig_filepath = os.getenv("KUBECONFIG") or "~/.kube/config" 15 | 16 | 17 | class KubernetesClient(object): 18 | 19 | def __init__(self): 20 | self.logger = logging.getLogger(__name__) 21 | try: 22 | config_file = os.path.expanduser(kubeconfig_filepath) 23 | config.load_kube_config(config_file=config_file) 24 | except: 25 | self.logger.warning("unable to load kube-config") 26 | 27 | self.v1 = client.CoreV1Api() 28 | self.v1Beta1 = client.AppsV1beta1Api() 29 | self.extensionsV1Beta1 = client.ExtensionsV1beta1Api() 30 | self.autoscalingV1Api = client.AutoscalingV1Api() 31 | self.rbacApi = client.RbacAuthorizationV1beta1Api() 32 | self.batchV1Api = client.BatchV1Api() 33 | self.batchV2Api = client.BatchV2alpha1Api() 34 | 35 | def get_resource(self, resource, namespace="all"): 36 | ret, resources = None, list() 37 | try: 38 | ret, namespaced_resource = self._call_api_client(resource) 39 | except ApiException as ae: 40 | self.logger.warning("resource autocomplete disabled, encountered " 41 | "ApiException", exc_info=1) 42 | except (NewConnectionError, MaxRetryError, ConnectTimeoutError): 43 | self.logger.warning("unable to connect to k8 cluster", exc_info=1) 44 | if ret: 45 | for i in ret.items: 46 | if namespace == "all" or not namespaced_resource: 47 | resources.append((i.metadata.name, i.metadata.namespace)) 48 | elif namespace == i.metadata.namespace: 49 | resources.append((i.metadata.name, i.metadata.namespace)) 50 | return resources 51 | 52 | def _call_api_client(self, resource): 53 | namespaced_resource = True 54 | 55 | if resource == "pod": 56 | ret = self.v1.list_pod_for_all_namespaces(watch=False) 57 | elif resource == "service": 58 | ret = self.v1.list_service_for_all_namespaces(watch=False) 59 | elif resource == "deployment": 60 | ret = self.v1Beta1.list_deployment_for_all_namespaces(watch=False) 61 | elif resource == "statefulset": 62 | ret = self.v1Beta1.list_stateful_set_for_all_namespaces(watch=False) 63 | elif resource == "node": 64 | namespaced_resource = False 65 | ret = self.v1.list_node(watch=False) 66 | elif resource == "namespace": 67 | namespaced_resource = False 68 | ret = self.v1.list_namespace(watch=False) 69 | elif resource == "daemonset": 70 | ret = self.extensionsV1Beta1.list_daemon_set_for_all_namespaces(watch=False) 71 | elif resource == "networkpolicy": 72 | ret = self.extensionsV1Beta1.list_network_policy_for_all_namespaces(watch=False) 73 | elif resource == "thirdpartyresource": 74 | namespaced_resource = False 75 | ret = self.extensionsV1Beta1.list_third_party_resource(watch=False) 76 | elif resource == "replicationcontroller": 77 | ret = self.v1.list_replication_controller_for_all_namespaces(watch=False) 78 | elif resource == "replicaset": 79 | ret = self.extensionsV1Beta1.list_replica_set_for_all_namespaces(watch=False) 80 | elif resource == "ingress": 81 | ret = self.extensionsV1Beta1.list_ingress_for_all_namespaces(watch=False) 82 | elif resource == "endpoints": 83 | ret = self.v1.list_endpoints_for_all_namespaces(watch=False) 84 | elif resource == "configmap": 85 | ret = self.v1.list_config_map_for_all_namespaces(watch=False) 86 | elif resource == "event": 87 | ret = self.v1.list_event_for_all_namespaces(watch=False) 88 | elif resource == "limitrange": 89 | ret = self.v1.list_limit_range_for_all_namespaces(watch=False) 90 | elif resource == "configmap": 91 | ret = self.v1.list_config_map_for_all_namespaces(watch=False) 92 | elif resource == "persistentvolume": 93 | namespaced_resource = False 94 | ret = self.v1.list_persistent_volume(watch=False) 95 | elif resource == "secret": 96 | ret = self.v1.list_secret_for_all_namespaces(watch=False) 97 | elif resource == "resourcequota": 98 | ret = self.v1.list_resource_quota_for_all_namespaces(watch=False) 99 | elif resource == "componentstatus": 100 | namespaced_resource = False 101 | ret = self.v1.list_component_status(watch=False) 102 | elif resource == "podtemplate": 103 | ret = self.v1.list_pod_template_for_all_namespaces(watch=False) 104 | elif resource == "serviceaccount": 105 | ret = self.v1.list_service_account_for_all_namespaces(watch=False) 106 | elif resource == "horizontalpodautoscaler": 107 | ret = self.autoscalingV1Api.list_horizontal_pod_autoscaler_for_all_namespaces(watch=False) 108 | elif resource == "clusterrole": 109 | namespaced_resource = False 110 | ret = self.rbacApi.list_cluster_role(watch=False) 111 | elif resource == "clusterrolebinding": 112 | namespaced_resource = False 113 | ret = self.rbacApi.list_cluster_role_binding(watch=False) 114 | elif resource == "job": 115 | ret = self.batchV1Api.list_job_for_all_namespaces(watch=False) 116 | elif resource == "cronjob": 117 | ret = self.batchV2Api.list_cron_job_for_all_namespaces(watch=False) 118 | elif resource == "scheduledjob": 119 | ret = self.batchV2Api.list_scheduled_job_for_all_namespaces(watch=False) 120 | else: 121 | return None, namespaced_resource 122 | return ret, namespaced_resource 123 | -------------------------------------------------------------------------------- /kubeshell/completer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function 2 | from subprocess import check_output 3 | from prompt_toolkit.completion import Completer, Completion 4 | from fuzzyfinder import fuzzyfinder 5 | import logging 6 | import shlex 7 | import json 8 | import os 9 | import os.path 10 | 11 | from kubeshell.parser import Parser 12 | from kubeshell.client import KubernetesClient 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class KubectlCompleter(Completer): 17 | 18 | def __init__(self): 19 | self.inline_help = True 20 | self.namespace = "" 21 | self.kube_client = KubernetesClient() 22 | 23 | try: 24 | DATA_DIR = os.path.dirname(os.path.realpath(__file__)) 25 | DATA_PATH = os.path.join(DATA_DIR, 'data/cli.json') 26 | with open(DATA_PATH) as json_file: 27 | self.kubectl_dict = json.load(json_file) 28 | self.parser = Parser(DATA_PATH) 29 | except Exception as ex: 30 | logger.error("got an exception" + ex.message) 31 | 32 | def set_inline_help(self, val): 33 | self.inline_help = val 34 | 35 | def set_namespace(self, namespace): 36 | self.namespace = namespace 37 | 38 | def get_completions(self, document, complete_event, smart_completion=None): 39 | word_before_cursor = document.get_word_before_cursor(WORD=True) 40 | cmdline = document.text_before_cursor.strip() 41 | try: 42 | tokens = shlex.split(cmdline) 43 | _, _, suggestions = self.parser.parse_tokens(tokens) 44 | valid_keys = fuzzyfinder(word_before_cursor, suggestions.keys()) 45 | for key in valid_keys: 46 | yield Completion(key, -len(word_before_cursor), display=key, display_meta=suggestions[key]) 47 | except ValueError: 48 | pass 49 | -------------------------------------------------------------------------------- /kubeshell/kubeshell.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, unicode_literals 2 | 3 | from prompt_toolkit import prompt 4 | from prompt_toolkit.history import FileHistory 5 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 6 | from prompt_toolkit.key_binding.defaults import load_key_bindings_for_prompt 7 | from prompt_toolkit.keys import Keys 8 | 9 | from kubeshell.style import StyleFactory 10 | from kubeshell.completer import KubectlCompleter 11 | from kubeshell.lexer import KubectlLexer 12 | from kubeshell.toolbar import Toolbar 13 | from kubeshell.client import KubernetesClient, kubeconfig_filepath 14 | 15 | import os 16 | import click 17 | import sys 18 | import subprocess 19 | import yaml 20 | import logging 21 | logger = logging.getLogger(__name__) 22 | 23 | inline_help = True 24 | registry = load_key_bindings_for_prompt() 25 | completer = KubectlCompleter() 26 | client = KubernetesClient() 27 | 28 | 29 | class KubeConfig(object): 30 | 31 | clustername = user = "" 32 | namespace = "default" 33 | current_context_index = 0 34 | current_context_name = "" 35 | 36 | @staticmethod 37 | def parse_kubeconfig(): 38 | if not os.path.exists(os.path.expanduser(kubeconfig_filepath)): 39 | return ("", "", "") 40 | 41 | with open(os.path.expanduser(kubeconfig_filepath), "r") as fd: 42 | docs = yaml.load_all(fd) 43 | for doc in docs: 44 | current_context = doc.get("current-context", "") 45 | contexts = doc.get("contexts") 46 | if contexts: 47 | for index, context in enumerate(contexts): 48 | if context['name'] == current_context: 49 | KubeConfig.current_context_index = index 50 | KubeConfig.current_context_name = context['name'] 51 | if 'cluster' in context['context']: 52 | KubeConfig.clustername = context['context']['cluster'] 53 | if 'namespace' in context['context']: 54 | KubeConfig.namespace = context['context']['namespace'] 55 | if 'user' in context['context']: 56 | KubeConfig.user = context['context']['user'] 57 | return (KubeConfig.clustername, KubeConfig.user, KubeConfig.namespace) 58 | return ("", "", "") 59 | 60 | @staticmethod 61 | def switch_to_next_cluster(): 62 | if not os.path.exists(os.path.expanduser(kubeconfig_filepath)): 63 | return 64 | 65 | with open(os.path.expanduser(kubeconfig_filepath), "r") as fd: 66 | docs = yaml.load_all(fd) 67 | for doc in docs: 68 | contexts = doc.get("contexts") 69 | if contexts: 70 | KubeConfig.current_context_index = (KubeConfig.current_context_index+1) % len(contexts) 71 | cluster_name = contexts[KubeConfig.current_context_index]['name'] 72 | kubectl_config_use_context = "kubectl config use-context " + cluster_name 73 | cmd_process = subprocess.Popen(kubectl_config_use_context, shell=True, stdout=subprocess.PIPE) 74 | cmd_process.wait() 75 | return 76 | 77 | @staticmethod 78 | def switch_to_next_namespace(current_namespace): 79 | namespace_resources = client.get_resource("namespace") 80 | namespaces = sorted(res[0] for res in namespace_resources) 81 | index = (namespaces.index(current_namespace) + 1) % len(namespaces) 82 | next_namespace = namespaces[index] 83 | fmt = "kubectl config set-context {} --namespace={}" 84 | kubectl_config_set_namespace = fmt.format(KubeConfig.current_context_name, next_namespace) 85 | cmd_process = subprocess.Popen(kubectl_config_set_namespace, shell=True, stdout=subprocess.PIPE) 86 | cmd_process.wait() 87 | 88 | 89 | class Kubeshell(object): 90 | 91 | clustername = user = "" 92 | namespace = "default" 93 | 94 | def __init__(self, refresh_resources=True): 95 | shell_dir = os.path.expanduser("~/.kube/shell/") 96 | self.history = FileHistory(os.path.join(shell_dir, "history")) 97 | if not os.path.exists(shell_dir): 98 | os.makedirs(shell_dir) 99 | self.toolbar = Toolbar(self.get_cluster_name, self.get_namespace, self.get_user, self.get_inline_help) 100 | 101 | @registry.add_binding(Keys.F4) 102 | def _(event): 103 | try: 104 | KubeConfig.switch_to_next_cluster() 105 | Kubeshell.clustername, Kubeshell.user, Kubeshell.namespace = KubeConfig.parse_kubeconfig() 106 | except Exception as e: 107 | logger.warning("failed switching clusters", exc_info=1) 108 | 109 | @registry.add_binding(Keys.F5) 110 | def _(event): 111 | try: 112 | KubeConfig.switch_to_next_namespace(Kubeshell.namespace) 113 | Kubeshell.clustername, Kubeshell.user, Kubeshell.namespace = KubeConfig.parse_kubeconfig() 114 | except Exception as e: 115 | logger.warning("failed namespace switching", exc_info=1) 116 | 117 | @registry.add_binding(Keys.F9) 118 | def _(event): 119 | global inline_help 120 | inline_help = not inline_help 121 | completer.set_inline_help(inline_help) 122 | 123 | @registry.add_binding(Keys.F10) 124 | def _(event): 125 | sys.exit() 126 | 127 | def get_cluster_name(self): 128 | return Kubeshell.clustername 129 | 130 | def get_namespace(self): 131 | return Kubeshell.namespace 132 | 133 | def get_user(self): 134 | return Kubeshell.user 135 | 136 | def get_inline_help(self): 137 | return inline_help 138 | 139 | def run_cli(self): 140 | 141 | def get_title(): 142 | return "kube-shell" 143 | 144 | logger.info("running kube-shell event loop") 145 | if not os.path.exists(os.path.expanduser(kubeconfig_filepath)): 146 | click.secho('Kube-shell uses {0} for server side completion. Could not find {0}. ' 147 | 'Server side completion functionality may not work.'.format(kubeconfig_filepath), 148 | fg='red', blink=True, bold=True) 149 | while True: 150 | global inline_help 151 | try: 152 | Kubeshell.clustername, Kubeshell.user, Kubeshell.namespace = KubeConfig.parse_kubeconfig() 153 | except: 154 | logger.error("unable to parse {} %s".format(kubeconfig_filepath), exc_info=1) 155 | completer.set_namespace(self.namespace) 156 | 157 | try: 158 | user_input = prompt('kube-shell> ', 159 | history=self.history, 160 | auto_suggest=AutoSuggestFromHistory(), 161 | style=StyleFactory("vim").style, 162 | lexer=KubectlLexer, 163 | get_title=get_title, 164 | enable_history_search=False, 165 | get_bottom_toolbar_tokens=self.toolbar.handler, 166 | vi_mode=True, 167 | key_bindings_registry=registry, 168 | completer=completer) 169 | except (EOFError, KeyboardInterrupt): 170 | sys.exit() 171 | 172 | if user_input == "clear": 173 | click.clear() 174 | elif user_input == "exit": 175 | sys.exit() 176 | 177 | # if execute shell command then strip "!" 178 | if user_input.startswith("!"): 179 | user_input = user_input[1:] 180 | 181 | if user_input: 182 | if '-o' in user_input and 'json' in user_input: 183 | user_input += ' | pygmentize -l json' 184 | p = subprocess.Popen(user_input, shell=True) 185 | p.communicate() 186 | -------------------------------------------------------------------------------- /kubeshell/lexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import 2 | from pygments.lexer import RegexLexer 3 | from pygments.lexer import words 4 | from pygments.token import Keyword, Name, Operator, Generic, Literal, Text 5 | 6 | from kubeshell.completer import KubectlCompleter 7 | 8 | class KubectlLexer(RegexLexer): 9 | """Provides highlighting for commands, subcommands, arguments, and options. 10 | 11 | """ 12 | completer = KubectlCompleter() 13 | 14 | tokens = { 15 | 'root': [ 16 | (words( 17 | tuple(['kubectl', 'clear', 'exit']), 18 | prefix=r'\b', 19 | suffix=r'\b'), 20 | Literal.String), 21 | # (words( 22 | # tuple(completer.all_commands), 23 | # prefix=r'\b', 24 | # suffix=r'\b'), 25 | # Name.Class), 26 | # (words( 27 | # tuple(completer.all_args), 28 | # prefix=r'\b', 29 | # suffix=r'\b'), 30 | # Name.Class), 31 | # (words( 32 | # tuple(completer.all_opts), 33 | # prefix=r'', 34 | # suffix=r'\b'), 35 | # Keyword), 36 | # (words( 37 | # tuple(completer.global_opts), 38 | # prefix=r'', 39 | # suffix=r'\b'), 40 | # Keyword), 41 | # Everything else 42 | (r'.*\n', Text), 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /kubeshell/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import traceback 5 | import logging.config 6 | 7 | if not os.path.exists(os.path.expanduser("~/.kube/shell")): 8 | try: 9 | os.makedirs(os.path.expanduser("~/.kube/shell")) 10 | except OSError: 11 | print("failed to make config dirs for kube-shell") 12 | traceback.print_exc(file=sys.stdout) 13 | 14 | logfile = os.path.expanduser("~/.kube/shell/error.log") 15 | loggingConf = { 16 | "version": 1, 17 | "formatters": { 18 | "default": { 19 | "format": "%(asctime)-15s [%(levelname)-4s] %(name)s %(funcName)s:%(lineno)s - %(message)s", 20 | } 21 | }, 22 | "handlers": { 23 | "null": { 24 | "class": "logging.NullHandler", 25 | "level": "ERROR" 26 | }, 27 | "file": { 28 | "class": "logging.handlers.RotatingFileHandler", 29 | "level": "INFO", 30 | "formatter": "default", 31 | "filename": logfile, 32 | "backupCount": 3, 33 | "maxBytes": 10485760 # 10MB 34 | } 35 | }, 36 | "loggers": { 37 | "": { 38 | "level": "ERROR", 39 | "handlers": ["file"], 40 | }, 41 | "urllib3": { 42 | "level": "ERROR", 43 | "handlers": ["file"], 44 | "propagate": False 45 | }, 46 | "kubeshell": { 47 | "level": "INFO", 48 | "handlers": ["file"], 49 | "propagate": False 50 | } 51 | }, 52 | } 53 | logging.config.dictConfig(loggingConf) -------------------------------------------------------------------------------- /kubeshell/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from __future__ import print_function, absolute_import, unicode_literals 6 | from kubeshell.kubeshell import Kubeshell 7 | 8 | import logging 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def cli(): 13 | kube_shell= Kubeshell() 14 | logger.info("session start") 15 | kube_shell.run_cli() 16 | 17 | if __name__ == "__main__": 18 | cli() 19 | -------------------------------------------------------------------------------- /kubeshell/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function 2 | import json 3 | import os 4 | 5 | import logging 6 | logger = logging.getLogger(__name__) 7 | 8 | from kubeshell.client import KubernetesClient 9 | 10 | 11 | class Option(object): 12 | """ Option represents an optional local flag in kubectl """ 13 | 14 | def __init__(self, name, helptext): 15 | self.name = name 16 | self.helptext = helptext 17 | 18 | 19 | class CommandTree(object): 20 | """ CommandTree represents the tree node of a kubectl command line """ 21 | 22 | def __init__(self, node=None, helptext=None, children=None, localFlags=None): 23 | self.node = node 24 | self.help = helptext 25 | self.children = children if children else list() 26 | self.localFlags = localFlags if localFlags else list() 27 | 28 | def __str__(self): 29 | return "Node: %s, Help: %s\n Flags: %s\n Children: %s" % (self.node, self.help, self.localFlags, self.children) 30 | 31 | 32 | class Parser(object): 33 | """ Parser builds and walks the syntax tree of kubectl """ 34 | 35 | def __init__(self, apiFile): 36 | self.json_api = apiFile 37 | self.schema = dict() 38 | self.globalFlags = list() 39 | with open(self.json_api) as api: 40 | self.schema = json.load(api) 41 | self.ast = CommandTree("kubectl") 42 | self.ast = self.build(self.ast, self.schema.get("kubectl")) 43 | self.kube_client = KubernetesClient() 44 | 45 | def build(self, root, schema): 46 | """ Build the syntax tree for kubectl command line """ 47 | if schema.get("subcommands") and schema["subcommands"]: 48 | for subcmd, childSchema in schema["subcommands"].items(): 49 | child = CommandTree(node=subcmd) 50 | child = self.build(child, childSchema) 51 | root.children.append(child) 52 | # {args: {}, options: {}, help: ""} 53 | root.help = schema.get("help") 54 | for name, desc in schema.get("options").items(): 55 | if root.node == "kubectl": # register global flags 56 | self.globalFlags.append(Option(name, desc["help"])) 57 | root.localFlags.append(Option(name, desc["help"])) 58 | for arg in schema.get("args"): 59 | node = CommandTree(node=arg) 60 | root.children.append(node) 61 | return root 62 | 63 | def print_tree(self, root, indent=0): 64 | indentter = '{:>{width}}'.format(root.node, width=indent) 65 | print(indentter) 66 | for child in root.children: 67 | self.print_tree(root=child, indent=indent+2) 68 | 69 | def parse_tokens(self, tokens): 70 | """ Parse a sequence of tokens 71 | 72 | returns tuple of (parsed tokens, suggestions) 73 | """ 74 | if len(tokens) == 1: 75 | return list(), tokens, {"kubectl": self.ast.help} 76 | else: 77 | tokens.reverse() 78 | parsed, unparsed, suggestions = self.treewalk(self.ast, parsed=list(), unparsed=tokens) 79 | if not suggestions and unparsed: 80 | # TODO: @vogxn: This is hack until we include expected value types for each option and argument. 81 | # Whenver we recieve no suggestions but are left with unparsed tokens we pop the value and walk the 82 | # tree again without values 83 | logger.debug("unparsed tokens remain, possible value encountered") 84 | unparsed.pop() 85 | parsed.reverse() 86 | unparsed.extend(parsed) 87 | logger.debug("resuming treewalk with tokens: %s", unparsed) 88 | return self.treewalk(self.ast, parsed=list(), unparsed=unparsed) 89 | else: 90 | return parsed, unparsed, suggestions 91 | 92 | def treewalk(self, root, parsed, unparsed): 93 | """ Recursively walks the syntax tree at root and returns 94 | the items parsed, unparsed and possible suggestions """ 95 | suggestions = dict() 96 | if not unparsed: 97 | logger.debug("no tokens left unparsed. returning %s, %s", parsed, suggestions) 98 | return parsed, unparsed, suggestions 99 | 100 | token = unparsed.pop().strip() 101 | logger.debug("begin parsing at %s w/ tokens: %s", root.node, unparsed) 102 | if root.node == token: 103 | logger.debug("root node: %s matches next token:%s", root.node, token) 104 | parsed.append(token) 105 | if self.peekForOption(unparsed): # check for localFlags and globalFlags 106 | logger.debug("option(s) upcoming %s", unparsed) 107 | parsed_opts, unparsed, suggestions = self.evalOptions(root, list(), unparsed[:]) 108 | if parsed_opts: 109 | logger.debug("parsed option(s): %s", parsed_opts) 110 | parsed.extend(parsed_opts) 111 | if unparsed and not self.peekForOption(unparsed): # unparsed bits without options 112 | logger.debug("begin subtree %s parsing", root.node) 113 | for child in root.children: 114 | parsed_subtree, unparsed, suggestions = self.treewalk(child, list(), unparsed[:]) 115 | if parsed_subtree: # subtree returned further parsed tokens 116 | parsed.extend(parsed_subtree) 117 | logger.debug("subtree at: %s has matches. %s, %s", child.node, parsed, unparsed) 118 | break 119 | else: 120 | # no matches found in command tree 121 | # return children of root as suggestions 122 | logger.debug("no matches in subtree: %s. returning children as suggestions", root.node) 123 | for child in root.children: 124 | suggestions[child.node] = child.help 125 | else: 126 | logger.debug("no token or option match") 127 | unparsed.append(token) 128 | return parsed, unparsed, suggestions 129 | 130 | def peekForOption(self, unparsed): 131 | """ Peek to find out if next token is an option """ 132 | if unparsed and unparsed[-1].startswith("--"): 133 | return True 134 | return False 135 | 136 | def evalOptions(self, root, parsed, unparsed): 137 | """ Evaluate only the options and return flags as suggestions """ 138 | logger.debug("parsing options at tree: %s with p:%s, u:%s", root.node, parsed, unparsed) 139 | suggestions = dict() 140 | token = unparsed.pop().strip() 141 | 142 | parts = token.partition('=') 143 | if parts[-1] != '': # parsing for --option=value type input 144 | token = parts[0] 145 | 146 | allFlags = root.localFlags + self.globalFlags 147 | for flag in allFlags: 148 | if flag.name == token: 149 | logger.debug("matched token: %s with flag: %s", token, flag.name) 150 | parsed.append(token) 151 | if self.peekForOption(unparsed): # recursively look for further options 152 | parsed, unparsed, suggestions = self.evalOptions(root, parsed, unparsed[:]) 153 | # elif token == "--namespace": 154 | # namespaces = [('default', None), ('minikube', None), ('gitlab', None)] # self.kube_client.get_resource("namespace") 155 | # suggestions = dict(namespaces) 156 | break 157 | else: 158 | logger.debug("no flags match, returning allFlags suggestions") 159 | for flag in allFlags: 160 | suggestions[flag.name] = flag.helptext 161 | 162 | if suggestions: # incomplete parse, replace token 163 | logger.debug("incomplete option: %s provided. returning suggestions", token) 164 | unparsed.append(token) 165 | return parsed, unparsed, suggestions 166 | 167 | if __name__ == '__main__': 168 | parser = Parser('/Users/tsp/workspace/py/kube-shell/kubeshell/data/cli.json') 169 | p, _, s = parser.treewalk(parser.ast, parsed=list(), unparsed=['--', '--tcp 900:8080', 'nodeport', 'service', 'create', 'kubectl']) 170 | print(p, s) 171 | -------------------------------------------------------------------------------- /kubeshell/style.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | from __future__ import print_function, absolute_import, unicode_literals 14 | from pygments.token import Token 15 | from pygments.util import ClassNotFound 16 | from pygments.styles import get_style_by_name 17 | from prompt_toolkit.styles import default_style_extensions, style_from_dict 18 | 19 | 20 | class StyleFactory(object): 21 | """Provide styles for the autocomplete menu and the toolbar. 22 | 23 | :type style: :class:`pygments.style.StyleMeta` 24 | :param style: Contains pygments style info. 25 | """ 26 | 27 | def __init__(self, style_name): 28 | self.style = self.style_factory(style_name) 29 | 30 | def style_factory(self, style_name): 31 | """Retrieve the specified pygments style. 32 | 33 | If the specified style is not found, the vim style is returned. 34 | 35 | :type style_name: str 36 | :param style_name: The pygments style name. 37 | 38 | :rtype: :class:`pygments.style.StyleMeta` 39 | :return: Pygments style info. 40 | """ 41 | try: 42 | style = get_style_by_name(style_name) 43 | except ClassNotFound: 44 | style = get_style_by_name('vim') 45 | 46 | # Create a style dictionary. 47 | styles = {} 48 | styles.update(style.styles) 49 | styles.update(default_style_extensions) 50 | t = Token 51 | styles.update({ 52 | t.Menu.Completions.Completion.Current: 'bg:#00aaaa #000000', 53 | t.Menu.Completions.Completion: 'bg:#008888 #ffffff', 54 | t.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000', 55 | t.Menu.Completions.Meta: 'bg:#00aaaa #ffffff', 56 | t.Scrollbar.Button: 'bg:#003333', 57 | t.Scrollbar: 'bg:#00aaaa', 58 | t.Toolbar: 'bg:#222222 #cccccc', 59 | t.Toolbar.Off: 'bg:#222222 #696969', 60 | t.Toolbar.On: 'bg:#222222 #ffffff', 61 | t.Toolbar.Search: 'noinherit bold', 62 | t.Toolbar.Search.Text: 'nobold', 63 | t.Toolbar.System: 'noinherit bold', 64 | t.Toolbar.Arg: 'noinherit bold', 65 | t.Toolbar.Arg.Text: 'nobold' 66 | }) 67 | 68 | return style_from_dict(styles) 69 | -------------------------------------------------------------------------------- /kubeshell/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | import pip 4 | import pexpect 5 | import unittest 6 | 7 | class CliTest(unittest.TestCase): 8 | 9 | def test_run_cli(self): 10 | self.cli = None 11 | self.step_run_cli() 12 | self.step_see_prompt() 13 | 14 | def step_run_cli(self): 15 | self.cli = pexpect.spawnu('kube-shell') 16 | 17 | def step_see_prompt(self): 18 | self.cli.expect('kube-shell> ') 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /kubeshell/toolbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, unicode_literals 2 | from pygments.token import Token 3 | from pygments.token import Keyword, Name, Operator, Generic, Literal, Text 4 | 5 | class Toolbar(object): 6 | """Show information about the aws-shell in a tool bar. 7 | 8 | :type handler: callable 9 | :param handler: Wraps the callable `get_toolbar_items`. 10 | 11 | """ 12 | 13 | def __init__(self, get_cluster_name, get_namespace, get_user, get_inline_help): 14 | self.handler = self._create_toolbar_handler(get_cluster_name, get_namespace, get_user, get_inline_help) 15 | 16 | def _create_toolbar_handler(self, get_cluster_name, get_namespace, get_user, get_inline_help): 17 | def get_toolbar_items(_): 18 | if get_inline_help(): 19 | help_token = Token.Toolbar.On 20 | help = "ON" 21 | else: 22 | help_token = Token.Toolbar.Off 23 | help = "OFF" 24 | 25 | return [ 26 | (Keyword, ' [F4] Cluster: '), 27 | (Token.Toolbar, get_cluster_name()), 28 | (Keyword, ' [F5] Namespace: '), 29 | (Token.Toolbar, get_namespace()), 30 | (Keyword, ' User: '), 31 | (Token.Toolbar, get_user()), 32 | (Keyword, ' [F9] In-line help: '), 33 | (help_token, '{0}'.format(help)), 34 | (Keyword, ' [F10] Exit ') 35 | ] 36 | 37 | return get_toolbar_items 38 | -------------------------------------------------------------------------------- /misc/python_eats_cobra.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "encoding/json" 6 | "io/ioutil" 7 | //"fmt" 8 | 9 | "github.com/spf13/pflag" 10 | "github.com/spf13/cobra" 11 | _ "k8s.io/client-go/plugin/pkg/client/auth" // kubectl auth providers. 12 | _ "k8s.io/kubernetes/pkg/client/metrics/prometheus" // for client metric registration 13 | "k8s.io/kubernetes/pkg/kubectl/cmd" 14 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 15 | _ "k8s.io/kubernetes/pkg/version/prometheus" // for version metric registration 16 | ) 17 | 18 | type Option struct { 19 | Help string `json:"help"` 20 | Name string `json:"name"` 21 | } 22 | 23 | type Command struct { 24 | Command string `json:"command"` 25 | Help string `json:"help"` 26 | Subcommands map[string]Command `json:"subcommands"` 27 | Args []string `json:"args"` 28 | Options map[string]Option `json:"options"` 29 | } 30 | 31 | var ( 32 | kubectl_cmd = Command{} 33 | globalFlagsSet = false 34 | ) 35 | 36 | func main() { 37 | cobraCmd := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, os.Stdout, os.Stderr) 38 | buildCmdMap(&kubectl_cmd, cobraCmd) 39 | cli := make(map[string]Command) 40 | cli["kubectl"] = kubectl_cmd 41 | b, err := json.Marshal(&cli) 42 | if err != nil { 43 | panic(err) 44 | } 45 | ioutil.WriteFile("cli.json", b, 0644) 46 | } 47 | 48 | func buildCmdMap(rootCmd *Command, cobraCmd *cobra.Command) { 49 | 50 | rootCmd.Command = cobraCmd.Name() 51 | rootCmd.Help = cobraCmd.Short 52 | rootCmd.Subcommands = make(map[string]Command) 53 | rootCmd.Options = make(map[string]Option, 0) 54 | rootCmd.Args = make([]string, 0) 55 | 56 | for _, subCobraCmd := range cobraCmd.Commands(){ 57 | subCmd := Command{} 58 | buildCmdMap(&subCmd, subCobraCmd) 59 | rootCmd.Subcommands[subCobraCmd.Name()] = subCmd 60 | } 61 | 62 | validArgs := cobraCmd.ValidArgs 63 | if len(validArgs) > 0 { 64 | for _, cobraArg := range validArgs { 65 | rootCmd.Args = append(rootCmd.Args, cobraArg) 66 | } 67 | } 68 | 69 | if cobraCmd.HasFlags() { 70 | flagVisitFuncton := func(f *pflag.Flag) { 71 | option := Option{} 72 | option.Name = "--" + f.Name 73 | option.Help = f.Usage 74 | rootCmd.Options[option.Name] = option 75 | } 76 | flagSet := cobraCmd.Flags() 77 | flagSet.VisitAll(flagVisitFuncton) 78 | } 79 | 80 | if globalFlagsSet != true { 81 | inheritedFlagSet := cobraCmd.InheritedFlags() 82 | inheritedFlagVisitFuncton := func(f *pflag.Flag) { 83 | option := Option{} 84 | option.Name = "--" + f.Name 85 | option.Help = f.Usage 86 | kubectl_cmd.Options[option.Name] = option 87 | } 88 | inheritedFlagSet.VisitAll(inheritedFlagVisitFuncton) 89 | globalFlagsSet = true 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.23 3 | 4 | [metadata] 5 | description-file = README.rst 6 | 7 | [bdist_wheel] 8 | universal = 1 9 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from kubeshell import __version__ 2 | from setuptools import setup, find_packages 3 | 4 | import sys 5 | 6 | version = sys.version_info 7 | error_msg = "kube-shell needs Python>=2.7.10. Found %s" % sys.version 8 | 9 | if version.major == 2: 10 | if version.minor < 7: 11 | sys.exit(error_msg) 12 | else: 13 | if version.micro < 10: 14 | sys.exit(error_msg) 15 | 16 | 17 | requires = [ 18 | 'prompt-toolkit>=1.0.10,<1.1.0', 19 | 'Pygments>=2.1.3,<3.0.0', 20 | 'fuzzyfinder>=1.0.0', 21 | 'click>=4.0,<7.0', 22 | 'kubernetes>=0.10.0,<3.0.0', 23 | ] 24 | 25 | setup( 26 | name='kube-shell', 27 | version=__version__, 28 | description='Kubernetes shell: An integrated shell for working with the Kubernetes CLI', 29 | author='Cloudnative Labs', 30 | url='https://github.com/cloudnativelabs/kube-shell', 31 | packages=find_packages(), 32 | package_data={'kubeshell': ['data/cli.json']}, 33 | zip_safe=False, 34 | install_requires=requires, 35 | entry_points={ 36 | 'console_scripts': [ 37 | 'kube-shell = kubeshell.main:cli', 38 | ] 39 | }, 40 | license="Apache License 2.0", 41 | keywords=('kubernetes', 'autocomplete', 'shell',), 42 | classifiers=( 43 | 'Development Status :: 3 - Alpha', 44 | 'Intended Audience :: Developers', 45 | 'Intended Audience :: System Administrators', 46 | 'Natural Language :: English', 47 | 'License :: OSI Approved :: Apache Software License', 48 | 'Programming Language :: Python', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3.3', 52 | 'Programming Language :: Python :: 3.4', 53 | 'Programming Language :: Python :: 3.5', 54 | 'Programming Language :: Python :: 3.6', 55 | ), 56 | ) 57 | --------------------------------------------------------------------------------