├── kubeterminal ├── __init__.py ├── permissions.py ├── state.py ├── windowCmd.py ├── nodes.py ├── globals.py ├── lexer.py ├── pods.py └── cmd.py ├── images ├── kubeterminal_01.png ├── kubeterminal_02.png ├── kubeterminal_03.png └── kubeterminal_05.png ├── requirements.txt ├── LICENSE ├── .gitignore ├── CHANGES.adoc ├── README.adoc └── kubeterminal.py /kubeterminal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/kubeterminal_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samisalkosuo/kubeterminal/HEAD/images/kubeterminal_01.png -------------------------------------------------------------------------------- /images/kubeterminal_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samisalkosuo/kubeterminal/HEAD/images/kubeterminal_02.png -------------------------------------------------------------------------------- /images/kubeterminal_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samisalkosuo/kubeterminal/HEAD/images/kubeterminal_03.png -------------------------------------------------------------------------------- /images/kubeterminal_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samisalkosuo/kubeterminal/HEAD/images/kubeterminal_05.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wcwidth==0.1.9 2 | ascii-graph==1.5.1 3 | prompt-toolkit==3.0.2 4 | pyperclip==1.7.0 5 | PyPubSub==4.0.3 -------------------------------------------------------------------------------- /kubeterminal/permissions.py: -------------------------------------------------------------------------------- 1 | from .cmd import isAllNamespaceForbidden 2 | from .cmd import isNodesForbidden 3 | 4 | def isForbiddenAllNamespace(): 5 | return isAllNamespaceForbidden() 6 | 7 | def isForbiddenNodes(): 8 | return isNodesForbidden() 9 | 10 | 11 | -------------------------------------------------------------------------------- /kubeterminal/state.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | #Application state on module 4 | 5 | current_namespace = "" 6 | selected_pod = "" 7 | selected_node = "all" 8 | searchString = "" 9 | cursor_line = 0 10 | content_mode = "" 11 | current_context = "" 12 | 13 | #variables that hold namespace and resource name that were selected before switching 14 | #to non-resource window like context 15 | resource_namespace = "" 16 | resource_resourceName = "" 17 | 18 | class State(object): 19 | 20 | def __init__(self): 21 | self.current_namespace = "" 22 | self.selected_pod = "" 23 | self.selected_node = "all" 24 | self.cursor_line = 0 25 | self.current_context = "" 26 | self.resource_namespace = "" 27 | self.resource_resourceName = "" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sami Salkosuo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | icp_login.* 2 | kubeterminal.bin 3 | tmp/ 4 | todo.md 5 | samples/ 6 | kubeterminal_output_* 7 | .cert.tmp 8 | oc_login* 9 | local/ 10 | output-file.txt 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | -------------------------------------------------------------------------------- /kubeterminal/windowCmd.py: -------------------------------------------------------------------------------- 1 | #from application import globals 2 | import kubeterminal.globals as globals 3 | from .pods import list as podList 4 | from .cmd import getResources, getContexts 5 | 6 | def windowExists(windowName): 7 | return windowName in globals.WINDOW_LIST 8 | 9 | #returns true if window mode resource is global, like storageclass 10 | def isGlobalResource(content_mode): 11 | 12 | isGlobal = False 13 | if content_mode == globals.WINDOW_SC or \ 14 | content_mode == globals.WINDOW_PV or \ 15 | content_mode == globals.WINDOW_NODE or \ 16 | content_mode == globals.WINDOW_CRD or \ 17 | content_mode == globals.WINDOW_NAMESPACE \ 18 | : 19 | isGlobal = True 20 | 21 | return isGlobal 22 | 23 | def getCommandWindowTitle(content_mode, namespace, selected_node, selected_resource): 24 | title = "" 25 | if content_mode == globals.WINDOW_POD: 26 | title = "NS: %s, NODE: %s, POD: %s" % (namespace,selected_node,selected_resource) 27 | else: 28 | resourceType = globals.WINDOW_COMMAND_WINDOW_TITLE[content_mode] 29 | title = "NS: %s, %s: %s" % (namespace,resourceType, selected_resource) 30 | 31 | return title 32 | 33 | def getResourceType(content_mode): 34 | resourceType = globals.WINDOW_RESOURCE_TYPE[content_mode] 35 | return resourceType 36 | 37 | def getWindowContentAndTitle(content_mode, namespace, selected_node): 38 | 39 | contentList = "" 40 | title = "" 41 | resourceType = None 42 | if content_mode == globals.WINDOW_POD: 43 | resourceType = globals.WINDOW_POD 44 | (contentList,title) = getPods(namespace,selected_node) 45 | 46 | if content_mode == globals.WINDOW_CONTEXT: 47 | resourceType = globals.WINDOW_CONTEXT 48 | (contentList,title) = getContextList() 49 | 50 | if resourceType == None: 51 | resourceType = globals.WINDOW_RESOURCES_WINDOW_TITLE[content_mode] 52 | (contentList,title) = getListAndTitle(resourceType, namespace) 53 | 54 | return (contentList, title) 55 | 56 | def getPods(namespace, nodes): 57 | contentList=podList(namespace,nodes) 58 | podCount = len(contentList.split("\n")) 59 | title="%d Pods (ns: %s, nodes: %s)" % (podCount, namespace, nodes) 60 | return (contentList, title) 61 | 62 | def getContextsList(): 63 | contentList=getContexts() 64 | return contentList 65 | 66 | def getContextList(): 67 | contentList=getContexts() 68 | podCount = len(contentList) 69 | title="%d Contexts" % (podCount) 70 | return ("\n".join(contentList), title) 71 | 72 | def getListAndTitle(resourceType, namespace): 73 | contentList=getResources(resourceType, namespace) 74 | podCount = len(contentList) 75 | title="%d %s (ns: %s)" % (podCount, resourceType, namespace) 76 | return ("\n".join(contentList), title) 77 | 78 | 79 | -------------------------------------------------------------------------------- /kubeterminal/nodes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .cmd import getNodes, describeNode, getDescribeNodes 3 | 4 | def describe(cmdOptions, selectedNode): 5 | node = "" 6 | options = cmdOptions.strip() 7 | if options == "": 8 | node = selectedNode 9 | else: 10 | node = options 11 | if node.find("all") > -1: 12 | return "Describing all nodes not (yet) implemented." 13 | return describeNode(node) 14 | 15 | def getWorkerNodeNames(): 16 | workerNodes = getNodes(noderole="worker") 17 | nodeNames = [] 18 | for node in workerNodes.split("\n"): 19 | if node != "": 20 | nodeFields=node.split() 21 | nodeName=nodeFields[0] 22 | nodeNames.append(nodeName) 23 | return nodeNames 24 | 25 | def describeNodes(noderole,params=[]): 26 | #describe nodes based on node role, for example: worker, master, proxy 27 | nodesDescribeText=getDescribeNodes(noderole) 28 | if "describe" in params: 29 | return nodesDescribeText 30 | 31 | startIndex=0 32 | endIndex=0 33 | singleNodeDescribeTexts=[] 34 | #get all node descriptions 35 | while endIndex>-1: 36 | startIndex=nodesDescribeText.find("Name:",endIndex) 37 | 38 | endIndex=nodesDescribeText.find("Name:",startIndex+1) 39 | if endIndex == -1: 40 | singleNodeDescribeTexts.append(nodesDescribeText[startIndex:]) 41 | else: 42 | singleNodeDescribeTexts.append(nodesDescribeText[startIndex:endIndex]) 43 | 44 | #loop through all nodes 45 | outputText="" 46 | cpuUsage=[] 47 | memoryUsage=[] 48 | #p = re.compile(r'^Name:\s+', re.M) 49 | for nodeDescription in singleNodeDescribeTexts: 50 | nameIndex=nodeDescription.find("Name:") 51 | nodeName=nodeDescription[6:nodeDescription.find("\n",nameIndex+6)] 52 | nodeName=nodeName.strip() 53 | ind=nodeDescription.find("Allocated resources:") 54 | allocatedResources=nodeDescription[ind:nodeDescription.find("Events:",ind)] 55 | ind=allocatedResources.find("cpu") 56 | cpu=allocatedResources[ind:allocatedResources.find("\n",ind)] 57 | cpu=re.sub(r'[a-zA-Z%()]', ' ', cpu) 58 | cpuUsage.append(cpu) 59 | ind=allocatedResources.find("memory") 60 | memory=allocatedResources[ind:allocatedResources.find("\n",ind)] 61 | memory=re.sub(r'[a-zA-Z%()]', ' ', memory) 62 | memoryUsage.append(memory) 63 | 64 | outputText=outputText+nodeName 65 | outputText=outputText+"\n" 66 | outputText=outputText+allocatedResources 67 | outputText=outputText+"\n" 68 | 69 | #total usage 70 | 71 | outputText=outputText+"\n" 72 | outputText=outputText+"Total CPU allocation (approx.):\n" 73 | outputText=outputText+getAllocatedResourcesString(cpuUsage) 74 | 75 | outputText=outputText+"\n" 76 | outputText=outputText+"Total memory allocation (approx.):\n" 77 | outputText=outputText+getAllocatedResourcesString(memoryUsage) 78 | 79 | return outputText 80 | 81 | def getAllocatedResourcesString(usage): 82 | totalAllocation=0 83 | totalAllocatable=0 84 | for allocation in usage: 85 | alloc=allocation.split() 86 | usedCores=int(alloc[0].strip()) 87 | totalAllocation=totalAllocation+usedCores 88 | used=int(alloc[1])/100.0 89 | totalAllocatable=totalAllocatable+int(usedCores/used) 90 | 91 | return " %d%% (%d/%d)\n" % (int(100*totalAllocation/totalAllocatable),totalAllocation,totalAllocatable) 92 | 93 | def list(): 94 | '''Return list of tuples of nodes: [(value,label),(value,label),...]''' 95 | 96 | nodes = [] 97 | nodesReady=0 98 | allNodes = getNodes() 99 | for node in allNodes.split("\n"): 100 | if node != "": 101 | nodeFields=node.split() 102 | readyString=nodeFields[1] 103 | if readyString == "Ready": 104 | nodesReady=nodesReady+1 105 | value="%s %s %s %s" % (nodeFields[0],readyString,nodeFields[2],nodeFields[4]) 106 | nodes.append((nodeFields[0],value)) 107 | 108 | nodes.insert(0,("workers","all worker nodes")) 109 | nodes.insert(0,("all","all, ready %d/%d" % (nodesReady,len(nodes)-1) )) 110 | 111 | return nodes 112 | 113 | -------------------------------------------------------------------------------- /kubeterminal/globals.py: -------------------------------------------------------------------------------- 1 | #global constants 2 | 3 | #Pod window contents 4 | WINDOW_POD="WINDOW_POD" 5 | WINDOW_SVC="WINDOW_SVC" 6 | WINDOW_CM="WINDOW_CM" 7 | WINDOW_SECRET="WINDOW_SECRET" 8 | WINDOW_STS="WINDOW_STS" 9 | WINDOW_RS="WINDOW_RS" 10 | WINDOW_DS="WINDOW_DS" 11 | WINDOW_PVC="WINDOW_PVC" 12 | WINDOW_PV="WINDOW_PV" 13 | WINDOW_DEPLOYMENT="WINDOW_DEPLOYMENT" 14 | WINDOW_SC="WINDOW_SC" 15 | WINDOW_JOB="WINDOW_JOB" 16 | WINDOW_CRONJOB="WINDOW_CRONJOB" 17 | WINDOW_ROLE="WINDOW_ROLE" 18 | WINDOW_ROLEBINDING="WINDOW_ROLEBINDING" 19 | WINDOW_SA="WINDOW_SA" 20 | WINDOW_PDB="WINDOW_PDB" 21 | WINDOW_ROUTE="WINDOW_ROUTE" 22 | WINDOW_INGRESS="WINDOW_INGRESS" 23 | WINDOW_NODE="WINDOW_NODE" 24 | WINDOW_CRD="WINDOW_CRD" 25 | WINDOW_NAMESPACE="WINDOW_NAMESPACE" 26 | 27 | #undocumented 28 | WINDOW_CONTEXT="WINDOW_CONTEXT" 29 | 30 | WINDOW_LIST=[WINDOW_POD, 31 | WINDOW_SVC, 32 | WINDOW_CM, 33 | WINDOW_SECRET, 34 | WINDOW_STS, 35 | WINDOW_RS, 36 | WINDOW_DS, 37 | WINDOW_PVC, 38 | WINDOW_PV, 39 | WINDOW_DEPLOYMENT, 40 | WINDOW_SC, 41 | WINDOW_JOB, 42 | WINDOW_CRONJOB, 43 | WINDOW_ROLE, 44 | WINDOW_ROLEBINDING, 45 | WINDOW_SA, 46 | WINDOW_PDB, 47 | WINDOW_ROUTE, 48 | WINDOW_INGRESS, 49 | WINDOW_NODE, 50 | WINDOW_CRD, 51 | WINDOW_NAMESPACE 52 | ] 53 | 54 | 55 | WINDOW_COMMAND_WINDOW_TITLE = { 56 | WINDOW_POD: "POD",#not used 57 | WINDOW_SVC: "SERVICE", 58 | WINDOW_CM: "CONFIGMAP", 59 | WINDOW_SECRET: "SECRET", 60 | WINDOW_STS: "STATEFULSET", 61 | WINDOW_RS: "REPLICASET", 62 | WINDOW_DS: "DAEMONSET", 63 | WINDOW_PVC: "PERSISTENTVOLUMECLAIM", 64 | WINDOW_PV: "PERSISTENTVOLUME", 65 | WINDOW_DEPLOYMENT: "DEPLOYMENT", 66 | WINDOW_SC: "STORAGECLASS", 67 | WINDOW_JOB: "JOB", 68 | WINDOW_CRONJOB: "CRONJOB", 69 | WINDOW_ROLE: "ROLE", 70 | WINDOW_ROLEBINDING: "ROLEBINDING", 71 | WINDOW_SA: "SERVICEACCOUNT", 72 | WINDOW_PDB: "PODDISRUPTIONBUDGET", 73 | WINDOW_ROUTE: "ROUTE", 74 | WINDOW_INGRESS: "INGRESS", 75 | WINDOW_NODE: "NODE", 76 | WINDOW_CRD: "CUSTOMRESOURCEDEFINITION", 77 | WINDOW_NAMESPACE: "NAMESPACE" 78 | 79 | } 80 | 81 | WINDOW_RESOURCE_TYPE = { 82 | WINDOW_POD: "pod", 83 | WINDOW_SVC: "svc", 84 | WINDOW_CM: "cm", 85 | WINDOW_SECRET: "secret", 86 | WINDOW_STS: "sts", 87 | WINDOW_RS: "rs", 88 | WINDOW_DS: "ds", 89 | WINDOW_PVC: "pvc", 90 | WINDOW_PV: "pv", 91 | WINDOW_DEPLOYMENT: "deployment", 92 | WINDOW_SC: "sc", 93 | WINDOW_JOB: "job", 94 | WINDOW_CRONJOB: "cronjob", 95 | WINDOW_ROLE: "role", 96 | WINDOW_ROLEBINDING: "rolebinding", 97 | WINDOW_SA: "sa", 98 | WINDOW_PDB: "pdb", 99 | WINDOW_ROUTE: "route", 100 | WINDOW_INGRESS: "ingress", 101 | WINDOW_NODE: "node", 102 | WINDOW_CRD: "crd", 103 | WINDOW_NAMESPACE: "namespace" 104 | 105 | } 106 | 107 | #plural 108 | WINDOW_RESOURCES_WINDOW_TITLE = { 109 | WINDOW_POD: "Pods", 110 | WINDOW_SVC: "Services", 111 | WINDOW_CM: "ConfigMaps", 112 | WINDOW_SECRET: "Secrets", 113 | WINDOW_STS: "StatefulSets", 114 | WINDOW_RS: "ReplicaSets", 115 | WINDOW_DS: "DaemonSets", 116 | WINDOW_PVC: "PersistentVolumeClaims", 117 | WINDOW_PV: "PersistentVolumes", 118 | WINDOW_DEPLOYMENT: "Deployments", 119 | WINDOW_SC: "StorageClasses", 120 | WINDOW_JOB: "Jobs", 121 | WINDOW_CRONJOB: "CronJobs", 122 | WINDOW_ROLE: "Roles", 123 | WINDOW_ROLEBINDING: "RoleBindings", 124 | WINDOW_SA: "ServiceAccounts", 125 | WINDOW_PDB: "PodDisruptionBudgets", 126 | WINDOW_ROUTE: "Routes", 127 | WINDOW_INGRESS: "Ingresses", 128 | WINDOW_NODE: "Nodes", 129 | WINDOW_CRD: "CustomResourceDefinitions", 130 | WINDOW_NAMESPACE: "Namespaces" 131 | 132 | } 133 | -------------------------------------------------------------------------------- /CHANGES.adoc: -------------------------------------------------------------------------------- 1 | == 0.31 2 | 3 | - Removed change kubeconfig keyboard shortcuts. Use 'kubeconfig ' command. 4 | - Added change context to context-command. 5 | - Bug fixes. 6 | - Removed Dockerfile and scripts. 7 | 8 | == 0.30 9 | 10 | - Find kubeconfig-files at the start of the program. 11 | - Added keyboard shortcuts to view and select kubeconfig. 12 | 13 | == 0.29 14 | 15 | - Bug fix when decoding secret/configmap values and certificates. 16 | - Refactoring. 17 | 18 | == 0.28 19 | 20 | - Bug fix when using OpenShift 4.10 client. 21 | - Refactoring. 22 | 23 | == 0.27 24 | 25 | * Added --kubeconfig and --current-kubeconfig options and kubeconfig-command. 26 | * Added shortcut to refresh namespace and node windows. 27 | * Improved performance by having some commands to execute as background processes. 28 | * Refactoring. 29 | 30 | == 0.26 31 | 32 | * Added shortcut to show available windows. 33 | * Added all-command to show all resources in selected namespace. 34 | 35 | == 0.25 36 | 37 | * Changed and keybindings to and . 38 | * Changed bindings to . 39 | * Changed other bindings to . 40 | 41 | == 0.24 42 | 43 | * Bug fixes. 44 | * Refactoring. 45 | 46 | == 0.23 47 | 48 | * Added version-command. 49 | * Added scroll resource and output window keybindings. 50 | * Refactoring. 51 | 52 | == 0.22 53 | 54 | * Added PersistentVolumeClaims to window-cmd. 55 | * Added PersistentVolumes to window-cmd. 56 | * Added Deployments to window-cmd. 57 | * Added StorageClasses to window-cmd. 58 | * Added Jobs to window-cmd. 59 | * Added CronJobs to window-cmd. 60 | * Added Roles to window-cmd. 61 | * Added RoleBindings to window-cmd. 62 | * Added ServiceAccounts to window-cmd. 63 | * Added PodDisruptionBudgets to window-cmd. 64 | * Added Routes to window-cmd. 65 | * Added Ingresses to window-cmd. 66 | * Added Nodes to window-cmd. 67 | * Added CustomResourceDefinitions to window-cmd. 68 | * Added Namespaces to window-cmd. 69 | * Removed node, svc and ingress commands. 70 | * Refactoring. 71 | 72 | == 0.21 73 | 74 | * Added contexts-command. 75 | * Shows help when starting KubeTerminal. 76 | * Added --no-help option to not show help when starting Kubeterminal. 77 | 78 | == 0.20 79 | 80 | * Shows now all pods after startup. 81 | * Added shortcuts to change window (Alt-1, Alt-2, and so on). 82 | 83 | == 0.19 84 | 85 | * Added --oc option to select oc-command instead of kubectl. 86 | * Check if user is forbidden to list nodes/namespace, if so then do not show them. 87 | 88 | == 0.18 89 | 90 | * When using OpenShift, if namespaces fails, then use projects. 91 | * Added wrap-command and shift-g shortcut to toggle wrapping in Output-window. 92 | 93 | == 0.17 94 | 95 | * Bug fix when using workers-command. 96 | * Added sf to window-cmd. 97 | * Added rs to window-cmd. 98 | * Added ds to window-cmd. 99 | * Internal changes. 100 | 101 | == 0.16 102 | 103 | * Refactoring. 104 | * Bug fix when describing pods and showing all namespaces. 105 | 106 | == 0.15 107 | 108 | * Added ctrl-y shortcut to show YAML. 109 | * Added window-command to set resource type (pod, svc, cm, secret) to show in window. 110 | * yaml, json, describe commands show selected resource type. 111 | * Added decode-command to decode base64 value in secret and config map. 112 | * Added cert-command to show certificate using openssl. 113 | * Refactoring. 114 | 115 | == 0.14 116 | 117 | * Shows namespace, node and pod in command window title. 118 | * Added the first command line argument to not show dynamic title. 119 | * Added command line arguments to set smaller window size. 120 | * Added svc-command to show services in selected namespace. 121 | * Added ingress-command to show ingresses in selected namespace. 122 | 123 | == 0.13 124 | 125 | * Added ku-command to execute kubectl in selected namespace. 126 | * Added secret-command to get and view secrets. 127 | * Added cm-command to get and view configmaps. 128 | * Added --cert option to secret-command to show TLS certificate using openssl. 129 | 130 | == 0.12 131 | 132 | * Pod status is now yellow if not all containers are running. 133 | 134 | == 0.11 135 | 136 | * Added clip-command to copy Output-window contents to clipboard. 137 | 138 | == 0.10 139 | 140 | * Added workers-command to show worker node resource allocation. 141 | 142 | == 0.9 143 | 144 | * Cursor line in pods-windows is now remembered when refreshing pods. 145 | 146 | == 0.8 147 | 148 | * Removed extra print-statement when deleting pod. 149 | * Added labels-command to show labels of selected pod. 150 | * Added top-command. 151 | * Added -g option to top-command to show graphics. 152 | 153 | == 0.7 154 | 155 | * Added exec-command to execute commands in a pod. 156 | 157 | == 0.6 158 | 159 | * Added --force to delete-command. 160 | * Added yaml-command to get YAML of selected pod. 161 | * Added json-command to get YAML of selected pod. 162 | 163 | == 0.5 164 | 165 | * Added save-command to save Output-window contents to a file. 166 | 167 | == 0.4 168 | 169 | * Added search-command. Bound to "/" key. 170 | 171 | == 0.3 172 | 173 | * Added cls-command to help-command. 174 | * Added more colors to Pods-window. 175 | * Added red to some error/exception lines in Output-window. 176 | * Added "all worker nodes" to Nodes-window. 177 | * Added pretty printing to Pods-window. 178 | * Added pod count to Pods-window. 179 | 180 | == 0.2 181 | 182 | * Added Shift-G key binding to go to the end of text in Output-window. 183 | * Added 'cls' command to clear Output-window. 184 | * Added cursorline to Pods window. 185 | 186 | == 0.1 187 | 188 | * Initial release. 189 | -------------------------------------------------------------------------------- /kubeterminal/lexer.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.lexers import Lexer 2 | from prompt_toolkit.styles.named_colors import NAMED_COLORS 3 | import re 4 | import kubeterminal.globals as globals 5 | 6 | class ResourceWindowLexer(Lexer): 7 | 8 | def windowColors(self,contentMode, line): 9 | 10 | if contentMode == globals.WINDOW_POD: 11 | if "CrashLoopBackOff" in line or "Terminating" in line: 12 | return [(NAMED_COLORS["Red"],line)] 13 | if "Completed" in line: 14 | return [(NAMED_COLORS["GreenYellow"],line)] 15 | #find out running status 16 | #assume that line includes something like 2/2 or 1/1 or 1/3 17 | #to show how many pods are running 18 | if self.isRunningEqualReady(line) == False: 19 | return [(NAMED_COLORS["Yellow"],line)] 20 | #All appear to be running, if Running on line then green 21 | if "Running" in line: 22 | return [(NAMED_COLORS["Green"],line)] 23 | 24 | if contentMode == globals.WINDOW_CM or \ 25 | contentMode == globals.WINDOW_SECRET or \ 26 | contentMode == globals.WINDOW_ROUTE or \ 27 | contentMode == globals.WINDOW_CRD or \ 28 | contentMode == globals.WINDOW_PDB or \ 29 | contentMode == globals.WINDOW_SA or \ 30 | contentMode == globals.WINDOW_ROLEBINDING or \ 31 | contentMode == globals.WINDOW_ROLE or \ 32 | contentMode == globals.WINDOW_CRONJOB or \ 33 | contentMode == globals.WINDOW_SC or \ 34 | contentMode == globals.WINDOW_INGRESS \ 35 | : 36 | #default green 37 | return [(NAMED_COLORS["Green"],line)] 38 | 39 | from .state import current_namespace 40 | offset = 0 41 | if current_namespace == "all-namespaces": 42 | offset = 1 43 | 44 | #split line 45 | fields = line.split() 46 | value = None 47 | desiredValue = None 48 | if contentMode == globals.WINDOW_NAMESPACE: 49 | value = fields[1] 50 | desiredValue = "Active" 51 | if contentMode == globals.WINDOW_NODE: 52 | value = fields[1] 53 | desiredValue = "Ready" 54 | if contentMode == globals.WINDOW_PV: 55 | value = fields[4] 56 | desiredValue = "Bound" 57 | if contentMode == globals.WINDOW_PVC: 58 | value = fields[1+offset] 59 | desiredValue = "Bound" 60 | 61 | if value != None and desiredValue != None: 62 | return self.checkSingleField(line, value, desiredValue) 63 | 64 | if contentMode == globals.WINDOW_JOB or \ 65 | contentMode == globals.WINDOW_DEPLOYMENT or \ 66 | contentMode == globals.WINDOW_STS \ 67 | : 68 | return self.checkRunningSlashReady(line) 69 | 70 | if contentMode == globals.WINDOW_SVC: 71 | if "NodePort" in line: 72 | return [(NAMED_COLORS["GreenYellow"],line)] 73 | else: 74 | return [(NAMED_COLORS["Green"],line)] 75 | 76 | array = [1,2] 77 | #check desired, current, etc fields 78 | if contentMode == globals.WINDOW_DS: 79 | array = [fields[1+offset],fields[2+offset],fields[3+offset],fields[4+offset],fields[5+offset]] 80 | if contentMode == globals.WINDOW_RS: 81 | array = [fields[1+offset],fields[2+offset],fields[3+offset]] 82 | 83 | if self.arrayValuesAreEqual(array) == True: 84 | return [(NAMED_COLORS["Green"],line)] 85 | 86 | #default/unknown yellow 87 | return [(NAMED_COLORS["Yellow"],line)] 88 | 89 | def lex_document(self, document): 90 | #colors = list(sorted(NAMED_COLORS, key=NAMED_COLORS.get)) 91 | def get_line(lineno): 92 | line = document.lines[lineno] 93 | #import content mode 94 | from .state import content_mode 95 | return self.windowColors(content_mode, line) 96 | 97 | return get_line 98 | 99 | def arrayValuesAreEqual(self,arrayOfValues): 100 | checkValue = arrayOfValues[0] 101 | for value in arrayOfValues: 102 | if value != checkValue: 103 | return False 104 | 105 | return True 106 | 107 | def isRunningEqualReady(self, line): 108 | 109 | matchObj = re.search( r'([0-9]+)/([0-9]+)', line) 110 | rv = False 111 | if matchObj: 112 | runningNow=int(matchObj.group(1)) 113 | runningTarget=int(matchObj.group(2)) 114 | rv = runningNow == runningTarget 115 | 116 | return rv 117 | 118 | def checkRunningSlashReady(self,line): 119 | 120 | if self.isRunningEqualReady(line) == True: 121 | return [(NAMED_COLORS["Green"],line)] 122 | 123 | #default yellow 124 | return [(NAMED_COLORS["Yellow"],line)] 125 | 126 | def checkSingleField(self,line,value,desiredValue): 127 | #if value equals desiredValue return Green line 128 | #else return yellow 129 | if value == desiredValue: 130 | return [(NAMED_COLORS["Green"],line)] 131 | #default yellow 132 | return [(NAMED_COLORS["Yellow"],line)] 133 | 134 | 135 | class OutputWindowLexer(Lexer): 136 | 137 | def lex_document(self, document): 138 | #colors = list(sorted(NAMED_COLORS, key=NAMED_COLORS.get)) 139 | def get_line(lineno): 140 | 141 | #default lexer used when search string not found or search not speficied 142 | def defaultLexer(line): 143 | if line.find("=== ") == 0: 144 | return [(NAMED_COLORS["Cyan"],line)] 145 | 146 | if line.find("TIMEOUT ") == 0: 147 | return [(NAMED_COLORS["Yellow"],line)] 148 | 149 | #TODO: some kind of configuration for error lines 150 | if lowerLine.startswith("error:") or lowerLine.find(" error ") > 0 or lowerLine.find(" error") > 0 or lowerLine.find("exception: ")>0: 151 | return [(NAMED_COLORS["Red"],line)] 152 | 153 | return [(defaultColor,line)] 154 | 155 | #default, white 156 | #defaultColor=NAMED_COLORS["White"] 157 | defaultColor="#bbbbbb" 158 | 159 | line = document.lines[lineno] 160 | lowerLine = line.lower() 161 | 162 | #import search string from state 163 | from .state import searchString 164 | if searchString != "": 165 | #TODO add case sensitivity as config option 166 | searchString = searchString.lower() 167 | 168 | searchStringLength = len(searchString) 169 | searchResultFormat = 'bg:ansibrightyellow ansiblack' 170 | startIndex = 0 171 | foundIndex = lowerLine.find(searchString,startIndex) 172 | formattedText = [] 173 | if foundIndex > -1: 174 | while foundIndex > -1: 175 | formattedText.append((defaultColor,line[startIndex:foundIndex])) 176 | #new start index is lenght of search string + found index 177 | startIndex = foundIndex + searchStringLength 178 | #found text 179 | formattedText.append((searchResultFormat,line[foundIndex:startIndex])) 180 | foundIndex = lowerLine.find(searchString,startIndex) 181 | else: 182 | return defaultLexer(line) 183 | #formattedText.append((defaultColor,line)) 184 | if startIndex > 0: 185 | #add end of the line using default format 186 | formattedText.append((defaultColor,line[startIndex:])) 187 | return formattedText 188 | else: 189 | return defaultLexer(line) 190 | # if line.find("=== ") == 0: 191 | # return [(NAMED_COLORS["Cyan"],line)] 192 | 193 | # if line.find("TIMEOUT ") == 0: 194 | # return [(NAMED_COLORS["Yellow"],line)] 195 | 196 | # #TODO: some kind of configuration for error lines 197 | # if lowerLine.find(" error ") > 0 or lowerLine.find("exception: ")>0: 198 | # return [(NAMED_COLORS["Red"],line)] 199 | 200 | # return [(defaultColor,line)] 201 | 202 | return get_line 203 | 204 | -------------------------------------------------------------------------------- /kubeterminal/pods.py: -------------------------------------------------------------------------------- 1 | 2 | from .cmd import getNamespaces,getPods 3 | from .cmd import getPodLabels,getTop 4 | from .nodes import getWorkerNodeNames 5 | 6 | 7 | def labels(podName,namespaceName): 8 | labelOutput = getPodLabels(podName,namespaceName) 9 | labelOutput = labelOutput.split("\n")[1].split()[5].split(",") 10 | labelOutput.sort() 11 | labelOutput = "\n".join(labelOutput) 12 | return labelOutput 13 | 14 | def top(podName,namespaceName,cmdString,isAllNamespaces=False,doAsciiGraph=False): 15 | output = getTop(podName,namespaceName,cmdString,isAllNamespaces) 16 | topOutput = output 17 | if doAsciiGraph == True: 18 | from ascii_graph import Pyasciigraph 19 | from ascii_graph.colors import Gre,Yel,Red 20 | from ascii_graph.colordata import vcolor 21 | from ascii_graph.colordata import hcolor 22 | 23 | graphSymbol='*' 24 | titleBarSymbol='-' 25 | graph = Pyasciigraph(titlebar=titleBarSymbol, graphsymbol=graphSymbol) 26 | 27 | output = "" 28 | #get data from top output 29 | cpuUsage = [] 30 | memoryUsage = [] 31 | cpuUsagePercentForNode = [] 32 | memoryUsagePercentForNode = [] 33 | lines = topOutput.split("\n")[1:] 34 | podName=None 35 | for line in lines: 36 | if(len(line)==0): 37 | continue 38 | fields=line.split() 39 | if cmdString.find("-c") > -1: 40 | podName=fields[0] 41 | cpuUse=(fields[1], int(fields[2].replace("m",""))) 42 | memUse=(fields[1], int(fields[3].replace("Mi",""))) 43 | elif cmdString.find("-n") > -1: 44 | #nodes, must be before isAllNamespaces check 45 | cpuUse=(fields[0], int(fields[1].replace("m",""))) 46 | memUse=(fields[0], int(fields[3].replace("Mi",""))) 47 | cpuUsagePercentForNode.append((fields[0], int(fields[2].replace("%","")))) 48 | memoryUsagePercentForNode.append((fields[0], int(fields[4].replace("%","")))) 49 | elif isAllNamespaces==True: 50 | rowTitle="%s/%s" % (fields[0],fields[1]) 51 | cpuUse=(rowTitle, int(fields[2].replace("m",""))) 52 | memUse=(rowTitle, int(fields[3].replace("Mi",""))) 53 | else: 54 | cpuUse=(fields[0], int(fields[1].replace("m",""))) 55 | memUse=(fields[0], int(fields[2].replace("Mi",""))) 56 | cpuUsage.append(cpuUse) 57 | memoryUsage.append(memUse) 58 | 59 | cpuTitle='CPU (millicores)' 60 | if podName != None: 61 | cpuTitle="%s - %s" % (cpuTitle,podName) 62 | for line in graph.graph(cpuTitle, cpuUsage): 63 | output = output + line + "\n" 64 | 65 | memTitle='Memory (Mi bytes)' 66 | if podName != None: 67 | memTitle="%s - %s" % (memTitle,podName) 68 | output= output + "\n" 69 | for line in graph.graph(memTitle, memoryUsage): 70 | output = output + line + "\n" 71 | 72 | if cmdString.find("-n") > -1: 73 | #add percents for nodes 74 | pattern = [Gre, Yel, Red] 75 | # Color lines according to Thresholds 76 | thresholds = { 77 | 0: Gre, 50: Yel, 80: Red 78 | } 79 | data = hcolor(cpuUsagePercentForNode, thresholds) 80 | graph = Pyasciigraph(force_max_value=100, titlebar=titleBarSymbol, graphsymbol=graphSymbol) 81 | data = cpuUsagePercentForNode 82 | output= output + "\n" 83 | cpuTitle='CPU (%)' 84 | for line in graph.graph(cpuTitle, data): 85 | output = output + line + "\n" 86 | 87 | output= output + "\n" 88 | memTitle='Memory (%)' 89 | for line in graph.graph(memTitle, memoryUsagePercentForNode): 90 | output = output + line + "\n" 91 | 92 | return output 93 | 94 | def list(namespace,nodehost=None): 95 | '''Return pods in namespace''' 96 | if nodehost == "all": 97 | nodehost=None 98 | 99 | nodeNames=None 100 | if nodehost == "workers": 101 | nodehost=None 102 | nodeNames = getWorkerNodeNames() 103 | #print(workerNodeNames) 104 | #kubectl get nodes -l node-role.kubernetes.io/worker=true 105 | #kubectl get pods --all-namespaces --no-headers --field-selector spec.nodeName=10.31.10.126 106 | #return "\n".join(workerNodeNames) 107 | 108 | podsString=getPods(namespace,nodeNameList=nodeNames) 109 | podsString=podsString.strip() 110 | 111 | 112 | podsList=[] 113 | if nodehost != None: 114 | #get pods in given nodehost 115 | pods=podsString.split('\n') 116 | for pod in pods: 117 | if pod.find(nodehost)>-1: 118 | podsList.append(pod) 119 | 120 | #return "\n".join(podsList) 121 | else: 122 | podsList=podsString.split('\n') 123 | 124 | 125 | #sort list 126 | podsList.sort() 127 | 128 | #TODO: do not show pods in openshift-* namespaces 129 | # newPodsList = [] 130 | # for pod in podsList: 131 | # if pod.startswith("openshift-") == False: 132 | # newPodsList.append(pod) 133 | # podsList = newPodsList 134 | 135 | #TODO: make toggle: show only Running/Completed pods or vice versa 136 | 137 | 138 | #podsListString = prettyPrint(podFieldsList(podsList),justify="L") 139 | #remove empty lines 140 | #podsListString = "".join([s for s in podsListString.strip().splitlines(True) if s.strip()]) 141 | 142 | 143 | #no prettyPrint, use output from get pods as it is 144 | podsListString = "\n".join(podsList) 145 | return podsListString 146 | 147 | 148 | def podFieldsList(podsList): 149 | #return list of pod dictioaries 150 | #not used yet 151 | podsFieldsList=[] 152 | #pods=podsString.split('\n') 153 | for pod in podsList: 154 | podFieldList=[] 155 | fields=pod.split() 156 | podsFieldsList.append(fields) 157 | # if len(fields) == 8: 158 | # #all namespaces 159 | # podDict["namespace"]=fields[0] 160 | # singeNamespaceOffset=0 161 | # if len(fields) == 7: 162 | # #single namespaces 163 | # singeNamespaceOffset=1 164 | 165 | # podDict["name"] = fields[1-singeNamespaceOffset] 166 | # ) 167 | # podDict["ready"] = fields[2-singeNamespaceOffset] 168 | # podDict["status"] = fields[3-singeNamespaceOffset] 169 | # podDict["restarts"] = fields[4-singeNamespaceOffset] 170 | # podDict["age"] = fields[5-singeNamespaceOffset] 171 | # podDict["pod_ip"] = fields[6-singeNamespaceOffset] 172 | # podDict["node_ip"] = fields[7-singeNamespaceOffset] 173 | 174 | # podsList.append(podDict) 175 | 176 | return podsFieldsList 177 | 178 | # Pretty Print table in tabular format 179 | # Original from: http://code.activestate.com/recipes/578801-pretty-print-table-in-tabular-format/ 180 | def prettyPrint(table, justify = "R", columnWidth = 0): 181 | 182 | try: 183 | #get max column widths 184 | defaultColumnWidth=15 #15 is length of IP address 185 | def maxColumnWidth(columnIndex): 186 | columnWidth=0 187 | for row in table: 188 | width = len(str(row[columnIndex])) 189 | if width > columnWidth: 190 | columnWidth = width 191 | return columnWidth 192 | 193 | #all column widths 194 | allWidths=[] 195 | try: 196 | for i in range(len(table[0])): 197 | allWidths.append(maxColumnWidth(i)) 198 | except: 199 | #if table is empty string, this will be catched 200 | #and empty string is returned 201 | return "" 202 | 203 | 204 | outputStr = "" 205 | for row in table: 206 | rowList = [] 207 | for i in range(len(row)): 208 | col = row[i] 209 | 210 | #for col in row: 211 | if i >= len(allWidths): 212 | columnWidth = allWidths[len(allWidths)-1] 213 | else: 214 | columnWidth = allWidths[i] 215 | if justify == "R": # justify right 216 | rowList.append(str(col).rjust(columnWidth)) 217 | elif justify == "L": # justify left 218 | rowList.append(str(col).ljust(columnWidth)) 219 | elif justify == "C": # justify center 220 | rowList.append(str(col).center(columnWidth)) 221 | outputStr += ' '.join(rowList) + "\n" 222 | except Exception as e: 223 | s = str(e) 224 | outputStr="jee%s\n%s" % (s,table) 225 | return outputStr 226 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = KubeTerminal 2 | :imagesdir: images/ 3 | :toc: 4 | 5 | KubeTerminal is helper tool for Kubernetes and/or OpenShift. The idea is provide a simple and quick tool to get the basics out of Kubernetes environment. 6 | 7 | KubeTerminal is complementing, not replacing, existing kubectl/oc and shell. 8 | 9 | Features include: 10 | 11 | * Uses the shell and kubectl-command. 12 | * List pods in namespace and/or node. 13 | * List services, secrets and other resources in namespace. 14 | * See pod logs. 15 | * Describe pods. 16 | ** Get YAML and JSON descriptions. 17 | ** Get labels. 18 | * Execute command in a pod. 19 | * View configmaps and secrets. 20 | ** Decode base64 values. 21 | ** Use openssl to view secret values that are TLS certificates. 22 | * Option for single executable, for easy installation. 23 | * Colors, if terminal supports them. 24 | 25 | image::kubeterminal_05.png[KubeTerminal_05,800] 26 | 27 | == Installation and usage 28 | 29 | * Python 3. 30 | ** Python 3.11.7 used for development. 31 | ** Windows or Linux. 32 | * Clone/download this repo. 33 | * Install prereqs: 34 | ** `pip install -r requirements.txt` 35 | ** Install and configure `kubectl` or `oc`. 36 | ** Install `openssl` in order to view certificates. 37 | * Login to Kubernetes/OpenShift cluster before using KubeTerminal. 38 | * Start KubeTerminal: 39 | ** `python kubeterminal.py` 40 | * Basic commands: 41 | ** refresh pod list: <ctrl-r> 42 | ** tab: switch windows and refresh resource list 43 | ** use tab to go to Pods-window and: 44 | *** line up/down to select pod 45 | *** describe pod: <ctrl-d> 46 | *** show logs: <ctrl-l> 47 | 48 | === KubeTerminal help 49 | 50 | ==== Command line options 51 | 52 | ``` 53 | usage: kubeterminal.py [-h] [--no-dynamic-title] [--compact-windows] 54 | [--even-more-compact-windows] 55 | [--kubeconfig KUBECONFIGPATH [KUBECONFIGPATH ...]] 56 | [--current-kubeconfig CURRENT_KUBECONFIG] [--oc] 57 | [--no-help] [--print-help] 58 | 59 | optional arguments: 60 | -h, --help show this help message and exit 61 | --no-dynamic-title Do not set command window title to show NS, node and 62 | pod. 63 | --compact-windows Set namespace, node and pod windows to more compact 64 | size. 65 | --even-more-compact-windows 66 | Set namespace, node and pod windows to even more 67 | compact size. 68 | --kubeconfig KUBECONFIGPATH [KUBECONFIGPATH ...] 69 | Set path(s) to kubeconfig auth file(s). 70 | --current-kubeconfig CURRENT_KUBECONFIG 71 | Set path to current/active kubeconfig auth file. 72 | --oc Use oc-command instead of kubectl. 73 | --no-help Do not show help when starting KubeTerminal. 74 | --print-help Print KubeTerminal help and exit. 75 | ``` 76 | 77 | ==== Terminal commands 78 | 79 | ``` 80 | === 2024-02-02T12:28:38.462310 help === 81 | KubeTerminal 82 | 83 | Helper tool for Kubernetes and OpenShift. 84 | 85 | Output window shows output of commands. 86 | "Selected pod/resource" is the resource where cursor is in the Resources window. 87 | 88 | Key bindings: 89 | 90 | ESC - exit program. 91 | TAB - change focus to another window. 92 | - resource window up one line. 93 | - resource window down one line. 94 | - resource window page up. 95 | - resource window page down. 96 | - output window page up. 97 | - output window page down. 98 | - list available windows. 99 | - show pods. 100 | - show configmaps. 101 | - show services. 102 | - show secrets. 103 | - show statefulsets. 104 | - show replicasets. 105 | - show daemonsets. 106 | - show persistentvolumeclaims. 107 | - show persistentvolumes. 108 | - show deployments. 109 | - show storageclasses. 110 | - show jobs. 111 | - show cronjobs. 112 | - show roles. 113 | - show rolebindings. 114 | - show serviceaccounts. 115 | - show poddisruptionbudgets. 116 | - show routes. 117 | - show ingresses. 118 | - show nodes. 119 | - show customresourcedefinitions. 120 | - show namespaces. 121 | - show kubeconfig and context. 122 | - show logs of currently selected pod. 123 | - refresh namespace and node windows. 124 | - show description of currently selected resource. 125 | - show YAML of currently selected resource. 126 | - refresh resource (pod etc.) list. 127 | - to the end of Output-window buffer. 128 | - toggle wrapping in Output-window. 129 | / - search string in Output-window. 130 | 131 | Commands: 132 | 133 | help - this help. 134 | all - show all resources in namespaces. 135 | clip - copy Output-window contents to clipboard. 136 | cls - clear Output-window. 137 | context [] - show current and available contexts or set current context. 138 | decode [cert} - decode base64 encoded secret or configmap value, optionally decode certificate. 139 | delete [--force] - delete currently selected pod, optionally force delete. 140 | describe - describe currently selected resource. 141 | exec [-c ] - exec command in currently selected pod. 142 | json - get JSON of currently selected resource. 143 | ku - execute kubectl in currently selected namespace. 144 | kubeconfig [] - list kubeconfigs or set current config. 145 | labels - show labels of currently selected pod. 146 | logs [-c ] - show logs of currently selected pod. 147 | oc - execute oc in currently selected namespace. 148 | save [] - save Output-window contents to a file. 149 | shell - executes any shell command. 150 | top [-c | -l | -n | -g] - show top of pods/containers/labels/nodes. Use -g to show graphics. 151 | version - Show 'kubectl' and 'oc' version information. 152 | window [ | list] - Set resource type for window. 'window list' lists available windows. 153 | workers [-d] - get worker node resource allocation. Use -d to describe all worker nodes. 154 | wrap - toggle wrapping in Output-window. 155 | yaml - get YAML of currently selected resource. 156 | ``` 157 | 158 | == Executable binary 159 | 160 | Executable binary is used to provide easy way to distribute KubeTerminal to servers without Internet connection. 161 | https://www.pyinstaller.org[PyInstaller] can be to create the executable 162 | 163 | Binary is created on system where you want to use the binary. For Windows binary, create the binary in Windows, for Linux, create the binary in Linux, and so on. 164 | 165 | === Create binary 166 | 167 | Use the following commands create binary in the platform you are using: 168 | 169 | * Install PyInstaller 170 | ** `pip install pyinstaller` 171 | * Create single file executable: 172 | ** `pyinstaller --onefile kubeterminal.py` 173 | * Binary file is located: 174 | ** `dist/kubeterminal` 175 | ** if building on Windows, file has _.exe_ suffix. 176 | 177 | == Screenshots 178 | 179 | image::kubeterminal_01.png[KubeTerminal_01] 180 | 181 | image::kubeterminal_02.png[KubeTerminal_02] 182 | 183 | image::kubeterminal_03.png[KubeTerminal_03] 184 | 185 | 186 | == Background 187 | 188 | I'm working with Kubernetes quite a lot and I found that there a few basic commands that I use very, very often. For example: 189 | 190 | * `kubectl get pods` 191 | * `kubectl logs ` 192 | * `kubectl describe pod ` 193 | 194 | Writing these commands take time, and when in hurry, that time is noticeable. 195 | 196 | I accidentally found https://github.com/astefanutti/kubebox[Kubebox] and immediately tried it. 197 | But authentication failed when using Kubernetes with self-signed certificate. 198 | 199 | Kubebox idea haunted until I remembered the existence of https://github.com/prompt-toolkit/python-prompt-toolkit[Python Prompt Toolkit] and remembered that it can be used to create full-screen terminal application. 200 | 201 | I decided to make my own Kubebox, and I named it KubeTerminal :-) 202 | -------------------------------------------------------------------------------- /kubeterminal/cmd.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output 2 | import subprocess 3 | import threading 4 | import locale 5 | import os 6 | from pubsub import pub 7 | import base64 8 | import binascii 9 | 10 | 11 | def getKubeConfigFile(): 12 | kubeconfigFile = os.environ.get("CURRENT_KUBECONFIG_FILE",None) 13 | if kubeconfigFile != None: 14 | return " --kubeconfig %s " % kubeconfigFile 15 | else: 16 | return "" 17 | 18 | def getKubectlCommand(): 19 | cmd = os.environ["KUBETERMINAL_CMD"] 20 | cmd = "%s %s" % (cmd, getKubeConfigFile()) 21 | return cmd 22 | 23 | #execute commands 24 | def executeCmd(cmd,timeout=30): 25 | 26 | #TODO: if output is very long, this will hang until it is done 27 | output = "" 28 | try: 29 | output = check_output(cmd,shell=True,stderr=subprocess.STDOUT,timeout=timeout) 30 | output = output.decode('utf-8') 31 | except subprocess.CalledProcessError as E: 32 | output = E.output.decode('utf-8') 33 | except subprocess.TimeoutExpired as E: 34 | output = E.output.decode('utf-8') 35 | output = "TIMEOUT when executing %s\n\n%s" % (cmd, output) 36 | except: 37 | #catch all exception including decoding errors 38 | #assume decoding error 39 | system_encoding = locale.getpreferredencoding() 40 | output = output.decode(system_encoding) 41 | # with open("output-file.txt", 'a') as out: 42 | # out.write(output + '\n') 43 | 44 | return output 45 | 46 | #Thanks go to: http://sebastiandahlgren.se/2014/06/27/running-a-method-as-a-background-thread-in-python/ 47 | class ExecuteCommandBackground(object): 48 | def __init__(self, cmd, publishOutput = False, publishTopic = 'print_output', decodeBase64 = False, decodeCert = False): 49 | self.cmd = cmd 50 | self.publishOutput = publishOutput 51 | self.publishTopic = publishTopic 52 | self.decodeBase64 = decodeBase64 53 | self.decodeCert = decodeCert 54 | if self.publishOutput == True: 55 | pub.sendMessage('background_processing_start',arg = self.cmd) 56 | thread = threading.Thread(target=self.run, args=()) 57 | thread.daemon = True # Daemonize thread 58 | thread.start() # Start the execution 59 | 60 | def run(self): 61 | output = executeCmd(self.cmd) 62 | if self.decodeBase64 == True: 63 | output = output.replace("'","") 64 | try: 65 | #image_data = base64.b64decode(my_image_string, validate=True) 66 | output = base64.b64decode(output,validate = True) 67 | output = str(output,"utf8") 68 | except binascii.Error: 69 | #string is not base64 70 | pass 71 | if self.decodeCert == True: 72 | #output is assumed to be certificate and openssl tool is assumed to present 73 | try: 74 | import subprocess,os 75 | fName = ".cert.tmp" 76 | certFile = open(fName,"w") 77 | certFile.write(output) 78 | certFile.close() 79 | output = subprocess.check_output(["openssl", "x509", "-text", "-noout", 80 | "-in", fName],stderr=subprocess.STDOUT,timeout=30) 81 | output = str(output,'utf-8') 82 | if os.path.exists(fName): 83 | os.remove(fName) 84 | except Exception as e: 85 | #catch all errors 86 | output = str(e) 87 | 88 | if self.publishOutput == True: 89 | pub.sendMessage('background_processing_stop',arg = self.cmd) 90 | pub.sendMessage(self.publishTopic,arg = output, arg2 = self.cmd) 91 | 92 | 93 | def executeBackgroudCmd(cmd): 94 | '''Execute command in background thread. Does not print output.''' 95 | ExecuteCommandBackground(cmd) 96 | return "Delete pod started in background. Refresh pod list to see status." 97 | 98 | def isAllNamespaceForbidden(): 99 | output = executeCmd("%s get namespaces" % getKubectlCommand()) 100 | return output.find("Forbidden") > -1 101 | 102 | def isNodesForbidden(): 103 | output = executeCmd("%s get nodes" % getKubectlCommand()) 104 | return output.find("Forbidden") > -1 105 | 106 | def deletePod(podName,namespace,force): 107 | cmd = getKubectlCommand() + " delete pod " + podName 108 | cmd=cmd + " -n " + namespace 109 | if (force == True): 110 | cmd=cmd + " --grace-period=0 --force" 111 | output = executeBackgroudCmd(cmd) 112 | return output 113 | 114 | def getPodLabels(podName,namespace): 115 | resourceType = "pod" 116 | 117 | cmd = getKubectlCommand() + " get %s %s -n %s --show-labels" % (resourceType, podName, namespace) 118 | output = executeCmd(cmd) 119 | 120 | return output 121 | 122 | def getTop(podName,namespace,cmdString,isAllNamespaces=False): 123 | cmd=None 124 | if cmdString.find("-c") > -1: 125 | #show top of selected pod and containers 126 | cmd = getKubectlCommand() + " top pod %s -n %s --containers" % (podName,namespace) 127 | 128 | if cmdString.find("-n") > -1: 129 | #show top of nodes 130 | cmd = getKubectlCommand() + " top nodes" 131 | 132 | if cmdString.find("-l") > -1: 133 | #show top of given labels 134 | label=cmdString.split()[2] 135 | cmd = getKubectlCommand() + " top pod -n %s -l %s" % (namespace,label) 136 | 137 | if cmd == None: 138 | if isAllNamespaces==True: 139 | cmd = getKubectlCommand() + " top pods --all-namespaces" 140 | else: 141 | cmd = getKubectlCommand() + " top pods -n %s" % namespace 142 | 143 | output = executeCmd(cmd) 144 | 145 | return output 146 | 147 | 148 | def execCmd(podName,namespace,command): 149 | cmd = getKubectlCommand() + " exec " + podName 150 | 151 | cmd=cmd+" -n " + namespace 152 | if (command.find("-c")==0): 153 | #there is container 154 | commandList=command.split() 155 | #first is -c 156 | #second is container name 157 | containerName=commandList[1] 158 | cmd=cmd+" -c %s -- %s " % (containerName," ".join(commandList[2:])) 159 | else: 160 | cmd=cmd+" -- " + command 161 | output = executeCmd(cmd) 162 | 163 | return output 164 | 165 | def getLogs(podName,namespace,options): 166 | cmd = getKubectlCommand() + " logs " + podName 167 | cmd = cmd +" -n "+namespace +" "+options 168 | ExecuteCommandBackground(cmd, publishOutput = True, publishTopic = 'print_logs') 169 | 170 | def getNodes(noderole=None): 171 | cmd = getKubectlCommand() + " get nodes " 172 | if noderole != None: 173 | cmd = "%s -l node-role.kubernetes.io/%s" % (cmd,noderole) 174 | output = executeCmd(cmd+" --no-headers") 175 | return output 176 | 177 | def describeNode(nodeName): 178 | cmd = getKubectlCommand() + " describe node \"%s\" " % nodeName 179 | output = executeCmd(cmd) 180 | return output 181 | 182 | def getDescribeNodes(noderole=None): 183 | cmd = getKubectlCommand() + " describe nodes " 184 | if noderole != None: 185 | cmd = "%s -l node-role.kubernetes.io/%s" % (cmd,noderole) 186 | output = executeCmd(cmd) 187 | return output 188 | 189 | 190 | def getPods(namespace,nodeNameList=[]): 191 | cmd = getKubectlCommand() + " get pods " 192 | 193 | if namespace == "all-namespaces": 194 | cmd=cmd+"--"+namespace 195 | else: 196 | cmd=cmd+"-n "+namespace 197 | cmd=cmd+" -o wide " 198 | cmd=cmd+" --no-headers" 199 | output = "" 200 | if nodeNameList != None and len(nodeNameList)>0: 201 | #get pods for specified nodes 202 | for nodeName in nodeNameList: 203 | cmd2="%s --field-selector spec.nodeName=%s" % (cmd,nodeName) 204 | output2 = executeCmd(cmd2) 205 | if output2.lower().find("no resources found") == -1: 206 | output = output + output2 207 | else: 208 | output = executeCmd(cmd) 209 | 210 | return output 211 | 212 | def getNamespaces(): 213 | namespaces=[] 214 | #executeBackgroudCmd 215 | #executeCmd 216 | output = executeCmd(getKubectlCommand() + " get namespaces --no-headers",timeout=5) 217 | if output.find("namespaces is forbidden") > -1: 218 | #OpenShift does not allow normal users to list namespaces 219 | #OpenShift has resource project that can be used 220 | output = executeCmd(getKubectlCommand() + " get projects --no-headers") 221 | 222 | for line in output.split('\n'): 223 | fields = line.split() 224 | if len(fields) > 0: 225 | namespaces.append(fields[0]) 226 | return namespaces 227 | 228 | def getResources(resourceType, namespace): 229 | contentList=[] 230 | namespaceOption = " -n %s " % namespace 231 | allNamespaceOption = "" 232 | if namespace == "all-namespaces": 233 | namespaceOption = "" 234 | allNamespaceOption = "--all-namespaces" 235 | output = executeCmd(getKubectlCommand() + " %s get %s --no-headers %s" % (namespaceOption, resourceType, allNamespaceOption)) 236 | for line in output.split('\n'): 237 | if len(line.split()) > 0: 238 | contentList.append(line) 239 | # fields = line.split() 240 | # if len(fields) > 0: 241 | # services.append(fields[0]) 242 | return contentList 243 | 244 | 245 | def getContexts(): 246 | contentList=[] 247 | output = executeCmd(getKubectlCommand() + " config get-contexts -o name") 248 | for line in output.split('\n'): 249 | if len(line.split()) > 0: 250 | contentList.append(line) 251 | return contentList 252 | 253 | def getCurrentContext(): 254 | contentList=[] 255 | output = executeCmd(getKubectlCommand() + " config current-context") 256 | return output.strip() 257 | 258 | def listNamespaces(): 259 | '''Return list of tuples of namespaces: [(value,label),(value,label),...]''' 260 | if isAllNamespaceForbidden() == True: 261 | namespaces = [] 262 | else: 263 | namespaces = [("all-namespaces","All namespaces")] 264 | allNamespaces = getNamespaces() 265 | 266 | for ns in allNamespaces: 267 | namespaces.append((ns,ns)) 268 | 269 | return namespaces -------------------------------------------------------------------------------- /kubeterminal.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import base64 3 | import re 4 | import argparse 5 | import os, sys 6 | import pathlib 7 | import csv 8 | 9 | from prompt_toolkit import Application 10 | from prompt_toolkit.buffer import Buffer 11 | from prompt_toolkit.layout.containers import HSplit,VSplit, Window 12 | from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl 13 | from prompt_toolkit.layout.layout import Layout 14 | from prompt_toolkit.key_binding import KeyBindings 15 | from prompt_toolkit.application import get_app 16 | from prompt_toolkit.widgets import SystemToolbar 17 | from prompt_toolkit.widgets import Frame, RadioList, VerticalLine, HorizontalLine, TextArea 18 | from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous 19 | from prompt_toolkit.keys import Keys 20 | from prompt_toolkit import eventloop 21 | from prompt_toolkit.shortcuts import yes_no_dialog 22 | from prompt_toolkit.utils import Event 23 | from prompt_toolkit.filters import to_filter 24 | #pubsub 25 | from pubsub import pub 26 | 27 | #set command before importing 28 | os.environ["KUBETERMINAL_CMD"] = "kubectl" 29 | 30 | from kubeterminal import pods,nodes,windowCmd 31 | from kubeterminal import state,lexer 32 | from kubeterminal import cmd 33 | from kubeterminal import globals 34 | from kubeterminal import permissions 35 | 36 | 37 | def fileExistsType(filePath): 38 | path = pathlib.Path(filePath) 39 | if path.is_file() == False: 40 | raise argparse.ArgumentTypeError('KUBECONFIG file %s does not exist.' % filePath) 41 | else: 42 | return filePath 43 | 44 | #CLI args 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument('--no-dynamic-title', action="store_true", help='Do not set command window title to show NS, node and pod.') 47 | parser.add_argument('--compact-windows', action="store_true", help='Set namespace, node and pod windows to more compact size.') 48 | parser.add_argument('--even-more-compact-windows', action="store_true", help='Set namespace, node and pod windows to even more compact size.') 49 | parser.add_argument('--ns-window-size', type=int, default=53, help='Namespace window size. Default is 53 or less.') 50 | parser.add_argument('--kubeconfig', action='append', nargs='+', type=fileExistsType, metavar='KUBECONFIGPATH', help='Set path(s) to kubeconfig auth file(s).') 51 | parser.add_argument('--current-kubeconfig', type=fileExistsType, help='Set path to current/active kubeconfig auth file.') 52 | parser.add_argument('--oc', action="store_true", help='Use oc-command instead of kubectl.') 53 | parser.add_argument('--no-help', action="store_true", help='Do not show help when starting KubeTerminal.') 54 | parser.add_argument('--print-help', action="store_true", help='Print KubeTerminal help and exit.') 55 | args = parser.parse_args() 56 | 57 | if args.oc == True: 58 | os.environ["KUBETERMINAL_CMD"] = "oc" 59 | 60 | kubeconfigFiles = [] 61 | #find all kubeconfig-files 62 | #assume all are named kubeconfig 63 | path = "/" 64 | for root,d_names,f_names in os.walk(path): 65 | for f in f_names: 66 | if f == "kubeconfig": 67 | kubeconfigFiles.append(os.path.join(root, f)) 68 | 69 | if args.kubeconfig: 70 | for kubeconfigArgs in args.kubeconfig: 71 | for kubeconfigFile in kubeconfigArgs: 72 | if not kubeconfigFile in kubeconfigFiles: 73 | kubeconfigFiles.append(kubeconfigFile) 74 | os.environ["KUBECONFIG_FILES"] = " ".join(kubeconfigFiles) 75 | 76 | try: 77 | if args.current_kubeconfig: 78 | os.environ["CURRENT_KUBECONFIG_FILE"] = "%s" % args.current_kubeconfig 79 | except: 80 | pass 81 | 82 | helpText = """KubeTerminal 83 | 84 | Helper tool for Kubernetes and OpenShift. 85 | 86 | Output window shows output of commands. 87 | "Selected pod/resource" is the resource where cursor is in the Resources window. 88 | 89 | Key bindings: 90 | 91 | ESC - exit program. 92 | TAB - change focus to another window. 93 | - resource window up one line. 94 | - resource window down one line. 95 | - resource window page up. 96 | - resource window page down. 97 | - output window page up. 98 | - output window page down. 99 | - list available windows. 100 | - show pods. 101 | - show configmaps. 102 | - show services. 103 | - show secrets. 104 | - show statefulsets. 105 | - show replicasets. 106 | - show daemonsets. 107 | - show persistentvolumeclaims. 108 | - show persistentvolumes. 109 | - show deployments. 110 | - show storageclasses. 111 | - show jobs. 112 | - show cronjobs. 113 | - show roles. 114 | - show rolebindings. 115 | - show serviceaccounts. 116 | - show poddisruptionbudgets. 117 | - show routes. 118 | - show ingresses. 119 | - show nodes. 120 | - show customresourcedefinitions. 121 | - show namespaces. 122 | - show kubeconfig and context. 123 | - show logs of currently selected pod. 124 | - refresh namespace and node windows. 125 | - show description of currently selected resource. 126 | - show YAML of currently selected resource. 127 | - refresh resource (pod etc.) list. 128 | - to the end of Output-window buffer. 129 | - toggle wrapping in Output-window. 130 | / - search string in Output-window. 131 | 132 | Commands: 133 | 134 | help - this help. 135 | all - show all resources in namespaces. 136 | clip - copy Output-window contents to clipboard. 137 | cls - clear Output-window. 138 | context [] - show current and available contexts or set current context. 139 | decode [cert} - decode base64 encoded secret or configmap value, optionally decode certificate. 140 | delete [--force] - delete currently selected pod, optionally force delete. 141 | describe - describe currently selected resource. 142 | exec [-c ] - exec command in currently selected pod. 143 | json - get JSON of currently selected resource. 144 | ku - execute kubectl in currently selected namespace. 145 | kubeconfig [] - list kubeconfigs or set current config. 146 | labels - show labels of currently selected pod. 147 | logs [-c ] - show logs of currently selected pod. 148 | oc - execute oc in currently selected namespace. 149 | save [] - save Output-window contents to a file. 150 | shell - executes any shell command. 151 | top [-c | -l | -n | -g] - show top of pods/containers/labels/nodes. Use -g to show graphics. 152 | version - Show 'kubectl' and 'oc' version information. 153 | window [ | list] - Set resource type for window. 'window list' lists available windows. 154 | workers [-d] - get worker node resource allocation. Use -d to describe all worker nodes. 155 | wrap - toggle wrapping in Output-window. 156 | yaml - get YAML of currently selected resource. 157 | 158 | """ 159 | 160 | if args.print_help == True: 161 | print(helpText) 162 | exit(0) 163 | 164 | from enum import Enum 165 | class WindowName(Enum): 166 | resource = "ResourceWindow" 167 | output = "OutputWindow" 168 | command = "CommandWindow" 169 | 170 | class Direction(Enum): 171 | up = "up" 172 | down = "down" 173 | 174 | 175 | applicationState = state#state.State() 176 | 177 | applicationState.content_mode=globals.WINDOW_POD 178 | 179 | #get max node length in window 180 | nodesList = nodes.list() 181 | 182 | longestNodeLine=0 183 | for node in nodesList: 184 | L = len(node[1]) 185 | if L > longestNodeLine: 186 | longestNodeLine = L 187 | #+8 added to inclucde radiolist checkboxes and window borders and space at the end of line 188 | longestNodeLine = longestNodeLine + 8 189 | if longestNodeLine > args.ns_window_size: 190 | longestNodeLine = args.ns_window_size 191 | 192 | namespaceWindowSize=27 193 | nodeWindowSize = longestNodeLine 194 | podListWindowSize = namespaceWindowSize + longestNodeLine #pod list window size max 80 195 | isCompactWindows=False 196 | if args.compact_windows == True: 197 | namespaceWindowSize=20 198 | nodeWindowSize=30 199 | podListWindowSize=50 200 | isCompactWindows=True 201 | if args.even_more_compact_windows == True: 202 | namespaceWindowSize=20 203 | nodeWindowSize=10 204 | podListWindowSize=30 205 | isCompactWindows=True 206 | if permissions.isForbiddenNodes() == True and isCompactWindows==False: 207 | namespaceWindowSize=80 208 | 209 | enableMouseSupport = False 210 | enableScrollbar = False 211 | 212 | #TODO: refactor code, all code 213 | 214 | def updateState(): 215 | 216 | selected_namespace=namespaceWindow.current_value 217 | selected_node=nodeListArea.current_value 218 | selected_pod=str(podListArea.buffer.document.current_line).strip() 219 | 220 | if applicationState.selected_pod != selected_pod: 221 | #somethingSelected=applicationState.selected_pod 222 | applicationState.selected_pod = selected_pod 223 | #f somethingSelected != "": 224 | # updateUI("selectedpod") 225 | 226 | if applicationState.current_namespace != selected_namespace: 227 | applicationState.current_namespace = selected_namespace 228 | #reset position 229 | state.cursor_line = -1 230 | 231 | updateUI("namespacepods") 232 | 233 | if applicationState.selected_node != selected_node: 234 | applicationState.selected_node = selected_node 235 | #reset position 236 | state.cursor_line = -1 237 | 238 | updateUI("nodepods") 239 | 240 | def updateUI(updateArea): 241 | 242 | if updateArea == "selectedpod": 243 | appendToOutput(applicationState.selected_pod) 244 | 245 | if updateArea == "nodepods" or updateArea == "namespacepods": 246 | moveToLine=state.cursor_line 247 | ns = applicationState.current_namespace 248 | contentList = "" 249 | title = "" 250 | (contentList,title) = windowCmd.getWindowContentAndTitle(applicationState.content_mode, ns,applicationState.selected_node) 251 | 252 | #TODO truncate long namespaces only in podlistarea, show full namespace in title window 253 | # if isAllNamespaces() == True: 254 | # rows = contentList.split("\n") 255 | # newContentList = [] 256 | # for row in rows: 257 | # columns = row.split() 258 | # namespace = columns[0] 259 | # namespace = (namespace[:25] + '..') if len(namespace) > 27 else namespace 260 | # namespace = namespace.ljust(27) 261 | 262 | # columns[0] = namespace 263 | # row = " ".join(columns) 264 | # newContentList.append(row) 265 | 266 | 267 | # contentList = "\n".join(newContentList) 268 | 269 | podListArea.text=contentList 270 | podListAreaFrame.title=title 271 | setCommandWindowTitle() 272 | if moveToLine > 0: 273 | #if pod window cursor line was greater than 0 274 | #then move to that line 275 | #appendToOutput("Should move to line: %d" % moveToLine) 276 | podListArea.buffer.cursor_down(moveToLine) 277 | 278 | 279 | kb = KeyBindings() 280 | # Global key bindings. 281 | 282 | @kb.add('tab') 283 | def tab_(event): 284 | updateState() 285 | #refresh UI 286 | focus_next(event) 287 | 288 | @kb.add('s-tab') 289 | def stab_(event): 290 | updateState() 291 | #refresh UI 292 | focus_previous(event) 293 | 294 | @kb.add('escape') 295 | def exit_(event): 296 | """ 297 | Pressing Esc will exit the user interface. 298 | 299 | Setting a return value means: quit the event loop that drives the user 300 | interface and return this value from the `CommandLineInterface.run()` call. 301 | """ 302 | event.app.exit() 303 | 304 | @kb.add('escape','d') 305 | def describepod_(event): 306 | applicationState.selected_pod=str(podListArea.buffer.document.current_line).strip() 307 | executeCommand("describe") 308 | 309 | @kb.add('escape','y') 310 | def yamlResource_(event): 311 | applicationState.selected_pod=str(podListArea.buffer.document.current_line).strip() 312 | executeCommand("yaml") 313 | 314 | @kb.add('escape','L') 315 | def logspod_(event): 316 | applicationState.selected_pod=str(podListArea.buffer.document.current_line).strip() 317 | executeCommand("logs") 318 | 319 | @kb.add('escape','r') 320 | def refreshpods_(event): 321 | #refresh pod window 322 | refreshWindows(refreshPodWindowOnly = True) 323 | 324 | @kb.add('escape','R') 325 | def refreshnsnodes_(event): 326 | #refresh namespace/node windows 327 | refreshWindows() 328 | 329 | @kb.add('escape','g') 330 | def toendofoutputbuffer_(event): 331 | outputArea.buffer.cursor_down(outputArea.document.line_count) 332 | 333 | @kb.add('escape','w') 334 | def togglewrap_(event): 335 | toggleWrap() 336 | 337 | #window scroll keybindings use alt modified and letters uiojkl 338 | #because various terminals have their own keybindings that interfere 339 | #for example, powershell has ctrl-j and putty/screen did not accept ctrl - cursor keys 340 | @kb.add('escape','j') 341 | def _(event): 342 | windowScroll(WindowName.resource, Direction.down) 343 | 344 | @kb.add('escape','k') 345 | def _(event): 346 | windowScroll(WindowName.resource, Direction.down, page = True) 347 | 348 | @kb.add('escape','u') 349 | def _(event): 350 | windowScroll(WindowName.resource, Direction.up) 351 | 352 | @kb.add('escape','i') 353 | def _(event): 354 | windowScroll(WindowName.resource, Direction.up, page = True) 355 | 356 | @kb.add('escape','l') 357 | def _(event): 358 | windowScroll(WindowName.output, Direction.down, page = True) 359 | 360 | @kb.add('escape','o') 361 | def _(event): 362 | windowScroll(WindowName.output, Direction.up, page = True) 363 | 364 | 365 | def changeWindow(windowName): 366 | updateState() 367 | executeCommand("window %s" % windowName) 368 | 369 | @kb.add('escape','c') 370 | def _(event): 371 | executeCommand("kubeconfig") 372 | executeCommand("context") 373 | 374 | @kb.add('escape','0') 375 | def _(event): 376 | executeCommand("windows") 377 | #key shortcuts to all windows alt-1 - alt-xx 378 | for index, windowName in enumerate(globals.WINDOW_LIST, start=1): 379 | 380 | if index < 10: 381 | #Alt-1 - 9 382 | @kb.add('escape',str(index)) 383 | def _(event): 384 | keySequence = event.key_sequence 385 | if len(keySequence) == 2: 386 | windowIndex = int(keySequence[1].key) - 1 387 | wn = globals.WINDOW_LIST[windowIndex] 388 | changeWindow(globals.WINDOW_RESOURCE_TYPE[wn]) 389 | 390 | else: 391 | #Alt-10 - 99 392 | numbers = str(index) 393 | @kb.add('escape',numbers[0],'escape',numbers[1]) 394 | def _(event): 395 | keySequence = event.key_sequence 396 | if len(keySequence) == 4: 397 | ki1 = int(keySequence[1].key) 398 | ki2 = int(keySequence[3].key) 399 | windowIndex = int("%d%d" % (ki1, ki2)) - 1 400 | wn = globals.WINDOW_LIST[windowIndex] 401 | changeWindow(globals.WINDOW_RESOURCE_TYPE[wn]) 402 | 403 | 404 | #search keyboard 405 | @kb.add('/') 406 | def searchbuffer_(event): 407 | #search both pods and output window at the same time 408 | if (len(command_container.text)>0): 409 | #if length of text is command container is > 0 410 | # assume that command is currently written 411 | #ignore search 412 | command_container.text=command_container.text+"/" 413 | command_container.buffer.cursor_right(len(command_container.text)) 414 | 415 | return 416 | 417 | layout.focus(command_container) 418 | command_container.text="/" 419 | command_container.buffer.cursor_right() 420 | 421 | def setCommandWindowTitle(): 422 | selected_namespace=namespaceWindow.current_value 423 | selected_node=nodeListArea.current_value 424 | selected_pod=str(podListArea.buffer.document.current_line).strip() 425 | 426 | if selected_namespace == "all-namespaces": 427 | fields = selected_pod.split() 428 | selected_namespace = fields[0] 429 | selected_pod = " ".join(fields[1:]) 430 | title = "" 431 | title = windowCmd.getCommandWindowTitle(applicationState.content_mode, selected_namespace, selected_node, selected_pod) 432 | 433 | if applicationState.content_mode == globals.WINDOW_CONTEXT: 434 | #select current context as title 435 | selected_pod = str(podListArea.buffer.document.current_line).strip() 436 | title = "Context: %s" % (selected_pod) 437 | #title = "Current Context: %s" % (applicationState.current_context) 438 | 439 | title = title.replace("", '') 440 | title = re.sub(' +', ' ', title) 441 | commandWindowFrame.title = title 442 | 443 | #listens cursor changes in pods list 444 | def podListCursorChanged(buffer): 445 | #when position changes, save cursor position to state 446 | state.cursor_line = buffer.document.cursor_position_row 447 | 448 | if args.no_dynamic_title == False: 449 | setCommandWindowTitle() 450 | 451 | def isAllNamespaces(): 452 | return applicationState.current_namespace == "all-namespaces" 453 | 454 | #scroll/move cursor in resource or output window 455 | def windowScroll(bufferName, direction, page = False): 456 | 457 | for w in layout.get_visible_focusable_windows(): 458 | try: 459 | buffer = w.content.buffer 460 | linesToScroll = 1 461 | if page == True: 462 | #if page is true then scroll page up or down 463 | renderInfo = w.render_info 464 | #window height is number of lines in the window 465 | windowHeight = renderInfo.window_height 466 | if direction == Direction.down: 467 | if buffer.document.cursor_position_row < windowHeight: 468 | linesToScroll = 2 * windowHeight - 2 469 | else: 470 | linesToScroll = windowHeight - 1 471 | if direction == Direction.up: 472 | linesToScroll = windowHeight - 1 473 | if buffer.document.cursor_position_row > buffer.document.line_count - windowHeight: 474 | linesToScroll = 2 * windowHeight - 2 475 | 476 | if (bufferName == WindowName.resource or bufferName == WindowName.output) and buffer.name == bufferName: 477 | if direction == Direction.down: 478 | buffer.cursor_down(linesToScroll) 479 | if direction == Direction.up: 480 | buffer.cursor_up(linesToScroll) 481 | 482 | except: 483 | #ignore errors, such as buffer not found in window 484 | pass 485 | 486 | 487 | def appendToOutput(text, cmdString = "", overwrite = False): 488 | 489 | if text is None or "No resources found" in text: 490 | return 491 | 492 | #TODO: option to set UTC or local 493 | #now = datetime.datetime.utcnow().isoformat() 494 | now = datetime.datetime.now().isoformat() 495 | if cmdString == "" or cmdString == None: 496 | header = "=== %s ===" % now 497 | else: 498 | header = "=== %s %s ===" % (now,cmdString) 499 | 500 | if outputArea.text == "": 501 | outputArea.text="\n".join([header,text,""]) 502 | else: 503 | outputArea.text="\n".join([outputArea.text,header,text,""]) 504 | 505 | # outputArea.buffer.cursor_position=len(outputArea.text) 506 | outputIndex=outputArea.text.find(header) 507 | outputArea.buffer.cursor_position=outputIndex#len(outputArea.text) 508 | outputArea.buffer.cursor_down(30) 509 | 510 | #TODO: combine this function, getKubectlCommand-function in cmd.py and also code in executeCommand-function. 511 | def getShellCmd(current_namespace, namespace, cmdString): 512 | 513 | if cmdString.find("ku ") == 0 or cmdString.find("oc ") == 0: 514 | cmdName = "kubectl" 515 | if cmdString.find("oc ") == 0: 516 | cmdName = "oc" 517 | #add kubeconfig 518 | cmdName = "%s %s" % (cmdName, cmd.getKubeConfigFile()) 519 | 520 | #command arguments af "oc" or "ku" 521 | cmdArgs = cmdString[2:].strip() 522 | #namespace argument added if not global resource like storageclass 523 | namespaceArg = "" 524 | if windowCmd.isGlobalResource(applicationState.content_mode) == False: 525 | # if current_namespace == "all-namespaces": 526 | # cmdArgs = "%s --all-namespaces" % cmdArgs 527 | # else: 528 | namespaceArg = "-n %s" % namespace 529 | 530 | cmdString = "shell %s %s %s" % (cmdName, namespaceArg, cmdArgs) 531 | 532 | return cmdString 533 | 534 | #command handler for shell 535 | def commandHander(buffer): 536 | #check incoming command 537 | cmdString = buffer.text 538 | executeCommand(cmdString) 539 | 540 | def refreshWindows(refreshPodWindowOnly = False): 541 | if refreshPodWindowOnly == False: 542 | cliApplication.refreshNamespaceAndNodeWindows() 543 | updateState() 544 | updateUI("namespacepods") 545 | 546 | 547 | #actual command handler, can be called from other sources as well 548 | def executeCommand(cmdString): 549 | import os#os imported also here because import at the beginning is not in this scope... 550 | refreshUIAfterCmd = False 551 | text="" 552 | cmdcmdString = cmdString.strip() 553 | originalCmdString = cmdcmdString 554 | if cmdString == "": 555 | return 556 | 557 | if cmdString == "help": 558 | text=helpText 559 | 560 | 561 | def getResourceNameAndNamespaceName(): 562 | 563 | if applicationState.content_mode == globals.WINDOW_CONTEXT: 564 | return (applicationState.resource_namespace,applicationState.resource_resourceName) 565 | 566 | podLine = applicationState.selected_pod 567 | 568 | namespace="" 569 | resourceName="" 570 | if podLine != "": 571 | fields=podLine.split() 572 | 573 | #if resource is global like storage class, 574 | #resource name is first field and there is no namespace 575 | if windowCmd.isGlobalResource(applicationState.content_mode) == True: 576 | resourceName=fields[0] 577 | namespace="" 578 | else: 579 | if applicationState.current_namespace == "all-namespaces": 580 | resourceName=fields[1] 581 | namespace=fields[0] 582 | else: 583 | resourceName=fields[0] 584 | namespace=applicationState.current_namespace 585 | 586 | return (namespace,resourceName) 587 | 588 | def getCmdString(cmd, resource): 589 | resourceType = windowCmd.getResourceType(applicationState.content_mode) 590 | 591 | if cmd == "describe": 592 | commandString ="ku describe %s %s" % (resourceType,resource) 593 | if cmd == "yaml": 594 | commandString ="ku get %s %s -o yaml" % (resourceType,resource) 595 | if cmd == "json": 596 | commandString ="ku get %s %s -o json" % (resourceType,resource) 597 | 598 | return commandString 599 | 600 | (namespace,resourceName)=getResourceNameAndNamespaceName() 601 | 602 | if cmdString.find("logs") == 0: 603 | if applicationState.content_mode == globals.WINDOW_POD: 604 | if namespace!="" and resourceName != "": 605 | options=cmdString.replace("logs","") 606 | cmd.getLogs(resourceName,namespace,options) 607 | else: 608 | text = "ERROR: Logs are available only for pods." 609 | 610 | if cmdString.find("describe") == 0: 611 | cmdString = getCmdString("describe",resourceName) 612 | 613 | if cmdString.find("yaml") == 0: 614 | cmdString = getCmdString("yaml",resourceName) 615 | 616 | if cmdString.find("json") == 0: 617 | cmdString = getCmdString("json",resourceName) 618 | 619 | if cmdString.find("label") == 0: 620 | if applicationState.content_mode == globals.WINDOW_POD: 621 | cmdString = "labels %s" % (resourceName) 622 | text=pods.labels(resourceName,namespace) 623 | else: 624 | text = "ERROR: Labels are currently available only for pods." 625 | 626 | doBase64decode = False 627 | decodeCert = False 628 | if cmdString.find("decode") == 0: 629 | if applicationState.content_mode == globals.WINDOW_SECRET or applicationState.content_mode == globals.WINDOW_CM: 630 | cmdArgs = cmdString.split() 631 | if len(cmdArgs) > 1: 632 | key = cmdArgs[1] 633 | cmdString ="" 634 | if applicationState.content_mode == globals.WINDOW_SECRET: 635 | cmdString = "secret " 636 | if applicationState.content_mode == globals.WINDOW_CM: 637 | cmdString = "cm " 638 | #cmdString = "%s %s %s --decode " % (cmdString,resourceName, key) 639 | cmdString = "%s %s %s " % (cmdString,resourceName, key) 640 | doBase64decode = True 641 | if cmdArgs[-1] == "cert": 642 | #decode command includes 'cert' => use openssl to show cert 643 | decodeCert = True 644 | else: 645 | text = "ERROR: No key name given." 646 | else: 647 | text = "ERROR: Decode available only for secrets and configmaps." 648 | 649 | #this command is used by decode and cert commands 650 | #these secret or cm commands not shown in help 651 | if cmdString.find("secret") == 0 or cmdString.find("cm") == 0: 652 | kubeArg = "secret" 653 | if cmdString.find("cm")==0: 654 | kubeArg = "cm" 655 | cmdStringList = cmdString.split() 656 | if len(cmdStringList) == 1: 657 | cmdString = "ku get %s" % kubeArg 658 | elif len(cmdStringList) == 2: 659 | cmdString = "ku get %s %s -o yaml" % (kubeArg, cmdStringList[1]) 660 | elif len(cmdStringList) >=3: 661 | jsonPath = cmdStringList[2] 662 | jsonPath = jsonPath.replace(".","\\.") 663 | cmdString = "ku get %s %s -o jsonpath='{.data.%s}'" % (kubeArg, cmdStringList[1], jsonPath) 664 | 665 | cmdString = getShellCmd(applicationState.current_namespace, namespace, cmdString) 666 | 667 | #if directly using oc or ku command do not add namespace 668 | #but add kubeconfig 669 | if originalCmdString.find("ku ") == 0: 670 | #kubectl 671 | cmdString = originalCmdString.replace("ku ","shell kubectl ") 672 | cmdString = "%s %s " % (cmdString, cmd.getKubeConfigFile()) 673 | 674 | if originalCmdString.find("oc ") == 0: 675 | #oc 676 | cmdString = originalCmdString.replace("oc ","shell oc ") 677 | cmdString = "%s %s " % (cmdString, cmd.getKubeConfigFile()) 678 | 679 | if cmdString.find("all") == 0: 680 | ns = "-n %s" % namespace 681 | if isAllNamespaces() == True: 682 | ns = "-A" 683 | cmdString = "shell oc %s get all %s" % (cmd.getKubeConfigFile(), ns) 684 | 685 | if cmdString.find("delete") == 0: 686 | if applicationState.content_mode == globals.WINDOW_POD: 687 | force=False 688 | if (cmdString.find("--force") > -1): 689 | force=True 690 | text=cmd.deletePod(resourceName,namespace,force) 691 | cmdString = "delete pod %s" % resourceName 692 | #refreshUIAfterCmd = True 693 | else: 694 | text = "ERROR: delete is available only for pods." 695 | 696 | if cmdString.find("shell") == 0: 697 | shellCmd = cmdString.replace("shell","").strip() 698 | #text=cmd.executeCmd(shellCmd) 699 | cmd.ExecuteCommandBackground(shellCmd, publishOutput = True,decodeBase64 = doBase64decode, decodeCert = decodeCert) 700 | 701 | if cmdString.find("version") == 0: 702 | text1=cmd.executeCmd("kubectl version") 703 | text2=cmd.executeCmd("oc version") 704 | text = "Kubernetes:\n%s\nOpenShift:\n%s" % (text1, text2) 705 | 706 | if cmdString.find("exec") == 0: 707 | if applicationState.content_mode == globals.WINDOW_POD: 708 | command = cmdString.replace("exec","").strip() 709 | cmdString = "exec %s %s" % (resourceName,command) 710 | text=cmd.execCmd(resourceName,namespace,command) 711 | else: 712 | text = "ERROR: exec is available only for pods." 713 | 714 | 715 | if cmdString.find("top") == 0: 716 | topCmd=cmdString 717 | if cmdString.find("-l") > -1: 718 | cmdString = cmdString.replace("-l","label") 719 | if cmdString.find("-c") > -1: 720 | cmdString = "top pod %s" % (resourceName) 721 | if cmdString.find("-n") > -1: 722 | cmdString = "top nodes" 723 | 724 | doAsciiGraph=False 725 | if topCmd.find("-g") > -1: 726 | doAsciiGraph = True 727 | topCmd = topCmd.replace("-g","") 728 | 729 | text=pods.top(resourceName,namespace,topCmd,isAllNamespaces(),doAsciiGraph) 730 | 731 | if cmdString.find("cls") == 0: 732 | clearOutputWindow() 733 | 734 | if cmdString.find("wrap") == 0: 735 | toggleWrap() 736 | 737 | if cmdString.find("/") == 0: 738 | #searching 739 | applicationState.searchString=cmdString[1:] 740 | #appendToOutput("TODO: search: %s" % applicationState.searchString, cmdString=cmdString) 741 | 742 | if cmdString.find("save") == 0: 743 | #save Output-window to a file 744 | cmdArgs = cmdString.split() 745 | if len(cmdArgs) > 1: 746 | #filename is the second argument 747 | filename = cmdArgs[1] 748 | else: 749 | filename = "kubeterminal_output_%s.txt" % datetime.datetime.now().strftime("%Y%m%d%H%M%S") 750 | with open(filename, "w") as outputFile: 751 | outputFile.write(outputArea.text) 752 | text="Output saved to file '%s'." % filename 753 | 754 | 755 | if cmdString.find("work") == 0: 756 | #worker node statistics 757 | 758 | params=[] 759 | if cmdString.find("-d") > -1: 760 | params.append("describe") 761 | 762 | nodeStats = nodes.describeNodes("worker",params) 763 | 764 | text=nodeStats 765 | 766 | if cmdString.find("clip") == 0: 767 | #copy output window contents to clipboard 768 | import pyperclip 769 | pyperclip.copy(outputArea.text) 770 | text="Output window contents copied to clipboard." 771 | 772 | if cmdString.find("win") == 0: 773 | #window command to select content for "pod"-window 774 | cmdArgs = cmdString.split() 775 | showAvailableWindowsHelpText = False 776 | if len(cmdArgs) > 1: 777 | windowName = "WINDOW_%s" % (cmdArgs[1].upper()) 778 | if windowCmd.windowExists(windowName) == True: 779 | applicationState.content_mode = windowName#"WINDOW_%s" % (windowName) 780 | #if window is context then update current context variable 781 | if applicationState.content_mode == globals.WINDOW_CONTEXT: 782 | applicationState.current_context = cmd.getCurrentContext() 783 | #store namespace and resource name that are selected before moving to non resource window like context 784 | (applicationState.resource_namespace,applicationState.resource_resourceName)=getResourceNameAndNamespaceName() 785 | text = "Current context:\n" + applicationState.current_context 786 | text = text+ "\n\nSelect context and enter command 'use-context' or use ctrl-u to change context." 787 | updateUI("namespacepods") 788 | else: 789 | windowArg = cmdArgs[1] 790 | if windowArg != "list": 791 | text = "Window '%s' does not exist.\n\n" % cmdArgs[1] 792 | showAvailableWindowsHelpText = True 793 | else: 794 | showAvailableWindowsHelpText = True 795 | text = "" 796 | if showAvailableWindowsHelpText == True: 797 | text = "%sAvailable windows:\n" % text 798 | for idx, resourceType in enumerate(globals.WINDOW_LIST): 799 | text="%swindow %s (Alt-%d)\n" % (text,resourceType.lower().replace("window_",""),idx+1) 800 | 801 | if cmdString.find("context") == 0: 802 | cmdArgs = cmdString.split() 803 | if len(cmdArgs) == 1: 804 | #context command to show contexts 805 | contextList = windowCmd.getContextsList() 806 | (text,title) = windowCmd.getContextList() 807 | text = "Available contexts:\n%s" % text 808 | currentContext = cmd.getCurrentContext() 809 | text = "Current context:\n%s\n\nAvailable contexts:\n" % (currentContext) 810 | i = 0 811 | for context in contextList: 812 | text = "%s%d: %s\n" % (text,i,context) 813 | i = i + 1 814 | text = "%s\n\nUse 'context ' command to change context." % (text) 815 | else: 816 | selectedContext=int(cmdString.split()[1]) 817 | contextList = windowCmd.getContextsList() 818 | selectedContext = contextList[selectedContext] 819 | cmdString = "kubectl config use-context %s" % (selectedContext) 820 | #cmd.ExecuteCommandBackground(cmdString, publishOutput = True,decodeBase64 = doBase64decode, decodeCert = decodeCert) 821 | cmd.executeCmd(cmdString) 822 | refreshWindows() 823 | 824 | if cmdString.find("kubeconfig") == 0: 825 | cmdArgs = cmdString.split() 826 | if len(cmdArgs) > 1: 827 | if "KUBECONFIG_FILES" in os.environ: 828 | #kubeconfig index given 829 | index = int(cmdArgs[1]) 830 | if index == 0: 831 | #clear kubeconfig 832 | if "CURRENT_KUBECONFIG_FILE" in os.environ: 833 | del os.environ["CURRENT_KUBECONFIG_FILE"] 834 | else: 835 | os.environ["CURRENT_KUBECONFIG_FILE"] = os.environ["KUBECONFIG_FILES"].split()[index-1] 836 | refreshWindows() 837 | else: 838 | text = "%sNo kubeconfigs available." % (text) 839 | else: 840 | #list given kubeconfigs 841 | currentContext = cmd.getCurrentContext() 842 | text = "Current context:\n%s\n\n" % (currentContext) 843 | text = "%sCurrent kubeconfig:\n" % (text) 844 | if "CURRENT_KUBECONFIG_FILE" in os.environ and os.environ["CURRENT_KUBECONFIG_FILE"] != None: 845 | text = "%s%s\n" % (text,os.environ["CURRENT_KUBECONFIG_FILE"]) 846 | else: 847 | text = "%s\n" % (text) 848 | text = "%s\nKubeconfigs:\n" % (text) 849 | text = "%s0: \n" % (text) 850 | if "KUBECONFIG_FILES" in os.environ: 851 | index = 1 852 | for cfg in os.environ["KUBECONFIG_FILES"].split(): 853 | text = "%s%d: %s\n" % (text, index, cfg) 854 | index = index + 1 855 | else: 856 | text = "%sNo kubeconfigs available." % (text) 857 | text = "%s\n\nUse 'kubeconfig ' command to change kubeconfig." % (text) 858 | 859 | if cmdString.find("login") == 0: 860 | text = "TODO: login command" 861 | 862 | #generic test commande 863 | if cmdString.find("testcmd") == 0: 864 | #text = cliApplication.refreshWindows() 865 | text="test command used during development" 866 | pub.sendMessage('working',arg="started") 867 | 868 | #refresh windows 869 | if cmdString.find("refresh") == 0: 870 | refreshWindows() 871 | 872 | if text != "": 873 | appendToOutput(text,cmdString=cmdString) 874 | #appendToOutput("\n".join([outputArea.text,text]),cmd=cmd) 875 | #outputArea.text="\n".join([outputArea.text,text]) 876 | 877 | if refreshUIAfterCmd == True: 878 | updateUI("namespacepods") 879 | 880 | def commandPrompt(line_number, wrap_count): 881 | return "command>" 882 | 883 | def clearOutputWindow(): 884 | outputArea.text = "" 885 | 886 | def toggleWrap(): 887 | outputArea.wrap_lines = not outputArea.wrap_lines 888 | 889 | #pubsub listeners and subscriptions 890 | def listener_print_logs(arg,arg2 = None): 891 | index = arg.find("choose one of: [") 892 | if index > -1: 893 | text1 = arg[0:index] 894 | text2 = arg[index:] 895 | text2 = text2.replace("choose one of: [","choose one of:\n[") 896 | text = "%s\n%s" % (text1, text2) 897 | else: 898 | text = arg 899 | appendToOutput(text, cmdString = arg2) 900 | pub.subscribe(listener_print_logs, 'print_logs') 901 | 902 | def listener_print_output(arg,arg2 = None): 903 | appendToOutput(arg, cmdString = arg2) 904 | pub.subscribe(listener_print_output, 'print_output') 905 | 906 | backgroundProcessesInProgress = 0 907 | def listener_background_processing_start(arg): 908 | global backgroundProcessesInProgress 909 | backgroundProcessesInProgress = backgroundProcessesInProgress + 1 910 | if (backgroundProcessesInProgress == 1): 911 | outputAreaFrame.title = "Output (Background process in progress)"#: %s)" % (outputAreaFrame.title, arg) 912 | if (backgroundProcessesInProgress > 1): 913 | outputAreaFrame.title = "Output (%d background processes in progress)" % backgroundProcessesInProgress#: %s)" % (outputAreaFrame.title, arg) 914 | 915 | def listener_background_processing_stop(arg=None): 916 | global backgroundProcessesInProgress 917 | backgroundProcessesInProgress = backgroundProcessesInProgress - 1 918 | if (backgroundProcessesInProgress == 0): 919 | outputAreaFrame.title = "Output" 920 | if (backgroundProcessesInProgress == 1): 921 | outputAreaFrame.title = "Output (Background process in progress)"#: %s)" % (outputAreaFrame.title, arg) 922 | if (backgroundProcessesInProgress > 1): 923 | outputAreaFrame.title = "Output (%d background processes in progress)" % backgroundProcessesInProgress#: %s)" % (outputAreaFrame.title, arg) 924 | 925 | pub.subscribe(listener_background_processing_start, 'background_processing_start') 926 | pub.subscribe(listener_background_processing_stop, 'background_processing_stop') 927 | 928 | 929 | #TODO: clarify UI creation 930 | 931 | def setNamespaceAndNodeWindowContents(): 932 | global namespaceList, nodesList, namespaceWindow, windowHeight, namespaceWindowFrame 933 | global nodeListArea, nodeWindowFrame, upper_left_container 934 | namespaceList = cmd.listNamespaces() 935 | nodesList = nodes.list() 936 | 937 | namespaceWindow = RadioList(namespaceList) 938 | windowHeight = len(namespaceList) + 2 939 | if windowHeight > 8: 940 | windowHeight = 8 941 | namespaceWindowFrame= Frame(namespaceWindow,title="Namespaces",height=windowHeight,width=namespaceWindowSize) 942 | 943 | nodeListArea = RadioList(nodesList) 944 | nodeWindowFrame= Frame(nodeListArea,title="Nodes",height=windowHeight,width=nodeWindowSize) 945 | #check permissions for nodes 946 | #normal OpenShift user does not see nodes nor namespaces other than his/her own. 947 | if permissions.isForbiddenNodes() == True: 948 | #if user can not see Nodes do not show node window 949 | upper_left_container = namespaceWindowFrame 950 | else: 951 | upper_left_container = VSplit([namespaceWindowFrame, 952 | #HorizontalLine(), 953 | #Window(height=1, char='-'), 954 | nodeWindowFrame]) 955 | 956 | #initialize namespace and node windows 957 | setNamespaceAndNodeWindowContents() 958 | 959 | #pods window 960 | podListArea = TextArea(text="", 961 | multiline=True, 962 | wrap_lines=False, 963 | scrollbar=enableScrollbar, 964 | lexer=lexer.ResourceWindowLexer(), 965 | read_only=True 966 | ) 967 | 968 | #add listener to cursor position changed 969 | podListArea.buffer.on_cursor_position_changed=Event(podListArea.buffer,podListCursorChanged) 970 | podListArea.buffer.name = WindowName.resource 971 | podListArea.window.cursorline = to_filter(True) 972 | podListAreaFrame = Frame(podListArea,title="Pods",width=podListWindowSize) 973 | 974 | #output area to output debug etc stuff 975 | outputArea = TextArea(text="", 976 | multiline=True, 977 | wrap_lines=False, 978 | lexer=lexer.OutputWindowLexer(), 979 | scrollbar=enableScrollbar, 980 | read_only=True) 981 | outputArea.buffer.name = WindowName.output 982 | outputAreaFrame= Frame(outputArea,title="Output") 983 | 984 | #command area 985 | command_container = TextArea(text="", multiline=False,accept_handler=commandHander,get_line_prefix=commandPrompt) 986 | command_container.buffer.name = WindowName.command 987 | commandWindowFrame= Frame(command_container,title="KubeTerminal (Ctrl-d to describe pod, Ctrl-l to show logs, Esc to exit, Tab to switch focus and refresh UI, 'help' for help)",height=4) 988 | 989 | 990 | def setLayoutContainers(): 991 | global left_container, content_container 992 | global root_container, layout 993 | 994 | left_container = HSplit([upper_left_container, 995 | #HorizontalLine(), 996 | #Window(height=1, char='-'), 997 | podListAreaFrame]) 998 | 999 | content_container = VSplit([ 1000 | # One window that holds the BufferControl with the default buffer on 1001 | # the left. 1002 | left_container, 1003 | # A vertical line in the middle. We explicitly specify the width, to 1004 | # make sure that the layout engine will not try to divide the whole 1005 | # width by three for all these windows. The window will simply fill its 1006 | # content by repeating this character. 1007 | #VerticalLine(), 1008 | #Window(width=1, char='|') 1009 | 1010 | # Display the text 'Hello world' on the right. 1011 | #Window(content=FormattedTextControl(text='Hello world, Escape to Quit')) 1012 | outputAreaFrame 1013 | 1014 | ]) 1015 | 1016 | root_container = HSplit([content_container, 1017 | # HorizontalLine(), 1018 | #Window(height=1, char='-'), 1019 | commandWindowFrame]) 1020 | 1021 | layout = Layout(root_container) 1022 | 1023 | #from 1024 | #https://stackoverflow.com/questions/47517328/prompt-toolkit-dynamically-add-and-remove-buffers-to-vsplit-or-hsplit 1025 | class MyApplication(Application): 1026 | 1027 | def __init__(self, layout, key_bindings, full_screen,mouse_support,before_render): 1028 | #Initialise with the first layout 1029 | super(MyApplication, self).__init__( 1030 | layout=layout, 1031 | key_bindings=key_bindings, 1032 | full_screen=full_screen, 1033 | mouse_support=mouse_support, 1034 | before_render=before_render 1035 | ) 1036 | 1037 | def refreshNamespaceAndNodeWindows(self): 1038 | setNamespaceAndNodeWindowContents() 1039 | setLayoutContainers() 1040 | # Update to use a new layout 1041 | self.layout = layout 1042 | 1043 | #set window containers 1044 | setLayoutContainers() 1045 | 1046 | #call 'before render'-function only when app is started the first time 1047 | #=> sets content to pod window 1048 | started=False 1049 | def before_render(application): 1050 | global started 1051 | if started == False: 1052 | updateState() 1053 | started = True 1054 | if args.no_help == False: 1055 | executeCommand("help") 1056 | #set started to env to be used in cmd.py 1057 | os.environ["KUBETERMINAL_IS_STARTED"] = "yes" 1058 | 1059 | cliApplication = MyApplication(layout=layout, 1060 | key_bindings=kb, 1061 | full_screen=True, 1062 | mouse_support=enableMouseSupport, 1063 | before_render=before_render 1064 | ) 1065 | 1066 | cliApplication.run() --------------------------------------------------------------------------------