├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── assets ├── kubernator.svg ├── kubernetes256.png └── screenshot.png ├── package-lock.json ├── package.json ├── src ├── FSProvider.ts ├── TreeDataProvider.ts ├── commands │ ├── clean.ts │ ├── create.ts │ ├── delete.ts │ ├── reveal.ts │ └── shell.ts ├── extension.ts ├── interfaces.ts ├── kube.ts └── util.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "lint", 22 | "problemMatcher": [ 23 | "$eslint-compact" 24 | ], 25 | "label": "npm: lint", 26 | "detail": "eslint src --ext ts" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [1.0.0] - 2022-06-27 7 | - remove deprecated `extensions` api group 8 | - remove *preview* flag 9 | 10 | ## [0.5.3] - 2022-06-15 11 | - clean more fields 12 | - bugfixes 13 | 14 | ## [0.5.2] - 2022-04-12 15 | - extract @smpio/kube package 16 | - handle errors on context switching 17 | - show "Kubernating..." on context switching 18 | - ignore non-critical API discovery errors 19 | 20 | ## [0.5.1] - 2022-04-11 21 | - updated dependencies 22 | - more tree decorations 23 | - fixed caching 24 | - fixed `Element with id is already registered` regression in [0.5.0] 25 | 26 | ## [0.5.0] - 2022-04-01 27 | - switching kubectl contexts 28 | - fixed possible situations, when "refresh" does nothing 29 | - improved Job manifest cleaning 30 | - "clean" now removes empty objects 31 | - improved Pod manifest cleaning 32 | - added status field near deployments, ds, etc 33 | - fixed object icons in tree, added colors depending on status 34 | - added status text to objects in tree 35 | 36 | ## [0.4.1] - 2021-07-13 37 | - open created manifest (command `Kubernator: Create`) in non-preview editor 38 | - fix creating objects in default namespace 39 | - base64 decode: convert value to map with single item: "decoded: ..." and handle this on save, also don't decode if there are non-printable characters 40 | 41 | ## [0.4.0] - 2021-06-12 42 | - disable "folding" of YAML block scalars, which replaces "|" with ">" 43 | - prevent saving of manifests in non-active tabs 44 | - secret encoding/decoding 45 | 46 | ## [0.3.3] - 2021-05-21 47 | - focus on reveal 48 | 49 | ## [0.3.2] - 2021-05-18 50 | - yes/no are booleans (https://github.com/kubernetes/kubernetes/issues/34146) 51 | 52 | ## [0.3.1] - 2021-05-16 53 | ### Fixed 54 | - some resources (kinds) may have schema only for non-preferred group version (e.g. batch/v1beta1 CronJob in 1.8) 55 | - prevent from outputting complex YAML keys 56 | 57 | ## [0.3.0] - 2021-05-16 58 | ### Added 59 | - strip `kubectl.kubernetes.io/last-applied-configuration` annotation 60 | 61 | ## [0.2.0] - 2021-05-14 62 | ### Changed 63 | - close active editor on create 64 | - [strip managedFields](https://github.com/kubernetes/kubernetes/pull/96878) 65 | ### Added 66 | - PVC -> right click -> Go to PV 67 | - reveal 68 | - namespace -> right click -> edit 69 | - right click on pod -> kubectl exec, kubectl debug 70 | - start kubectl proxy with extension 71 | ### Fixed 72 | - metadata.selfLink is deprecated 73 | 74 | ## [0.1.1] - 2021-05-13 75 | ### Changed 76 | - Updated documentation 77 | 78 | ## [0.1.0] - 2021-05-13 79 | - Initial release 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dmitry Bashkatov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernator 2 | 3 | Kubernetes object tree viewer and manifest editor. Lightweight and fast, compared to similar extensions on the marketplace. 4 | 5 | ![Screenshot](assets/screenshot.png) 6 | 7 | 8 | ## Features 9 | 10 | * view your cluster objects grouped by namespace, including Custom Resources 11 | * edit, create, delete object manifests and apply them to your cluster 12 | * clean object manifest, stripping read-only fields and fields with default values 13 | * RBAC friendly 14 | * multilpe clusters, but only one active per vscode instance for solidity 15 | * uses `kubectl proxy` to be fast and friendly 16 | 17 | 18 | ## Requirements 19 | 20 | `kubectl` should be in the PATH and authorized. 21 | 22 | 23 | ## Example usage 24 | 25 | ### Edit object 26 | 27 | 1. Select some object in tree view, e.g. any Deployment. A manifest editor will open. 28 | 2. Make changes to the manifest. 29 | 3. Save the manifest as always. The changes will apply to your cluster. 30 | 31 | ### Clone and edit object 32 | 33 | 1. Select some object in tree view, e.g. any Deployment. A manifest editor will open. 34 | 2. Run command `Kubernator: Clean` in command palette. A new tab with cleaned manifest will open. 35 | 3. Make changes to the manifest. 36 | 4. Run command `Kubernator: Create` in command palette. New object will be created in your cluster and a new tab with this object will open. 37 | 38 | 39 | ## Commands 40 | 41 | Important commands: 42 | 43 | * `Kubernator: Create`: analogue of `kubectl create -f manifest.yaml` for active editor 44 | * `Kubernator: Clean`: clean manifest in active editor, deleting read-only fields and fields with defaults 45 | * `Kubernator: Delete`: delete object in active editor 46 | * `Kubernator: Reveal`: reveal object in active editor 47 | * `Kubernator: Switch context`: select `kubectl` context 48 | * `Kubernator: Reconfigure`: reload API resources and manifest schema (executed on startup, maybe required after CRD installation or cluster upgrade) 49 | 50 | 51 | ## Context menu actions 52 | 53 | * `Pod` → `Shell`: create terminal with command `kubectl -n NS exec -it POD_NAME -- sh` 54 | * `PVC` → `Go to PV`: reveal PV bound to the PVC 55 | 56 | 57 | ## Settings 58 | 59 | See **Feature Contributions** extension page. 60 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - if object is changed during save, try to save if it is safe (automerge) https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_preventing-dirty-writes (SEE BRANCHES) 4 | - compare with saved (compare dirty) (SEE BRANCHES) 5 | 6 | - extract Pod from Deployment, StatefulSet, etc 7 | - update packages (see new `yo code` output) 8 | - on save conflict add option to overwrite (this can be done by removing resourceVersion) (SEE BRANCHES) 9 | - special view: some objects can have special view (simple describe, or complex graphs) 10 | 11 | - fsprovider -> readDirectory (for topbar nav) 12 | - shadow non-interesting fields, make this configurable (maybe shadow defaults) 13 | - links https://github.com/Azure/vscode-kubernetes-tools/blob/master/src/kuberesources.linkprovider.ts 14 | - yaml schemas https://github.com/Azure/vscode-kubernetes-tools/tree/master/src/yaml-support 15 | 16 | - load resources using api group with only preferredVersion, load non-preferred when needed only 17 | - tree quick search 18 | - limit concurrent requests to API - why? 19 | 20 | 21 | ## Can't be done 22 | 23 | Or workaround not found. 24 | 25 | - editor looses cursor position after saving existing object 26 | 27 | Document is reloaded with changes from API server after save and this resets cursor. 28 | 29 | - refresh tree on save/delete 30 | 31 | See branches refresh and refresh2 and https://stackoverflow.com/questions/67636127/vscode-extensions-treedataprovider-inconsistency 32 | -------------------------------------------------------------------------------- /assets/kubernator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 32 | 35 | 39 | 40 | 41 | 65 | 67 | 68 | 70 | image/svg+xml 71 | 73 | 74 | 75 | 76 | 77 | 82 | 84 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /assets/kubernetes256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smpio/kubernator-vscode/7d23d19ed5b528747424076ec8b854104ea1ffdd/assets/kubernetes256.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smpio/kubernator-vscode/7d23d19ed5b528747424076ec8b854104ea1ffdd/assets/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernator-vscode", 3 | "displayName": "Kubernator", 4 | "description": "Kubernetes object tree editor", 5 | "version": "1.0.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/smpio/kubernator-vscode.git" 9 | }, 10 | "publisher": "smpio", 11 | "engines": { 12 | "vscode": "^1.66.0" 13 | }, 14 | "license": "SEE LICENSE IN LICENSE", 15 | "categories": [ 16 | "Other" 17 | ], 18 | "keywords": [ 19 | "kubernetes" 20 | ], 21 | "preview": false, 22 | "icon": "assets/kubernetes256.png", 23 | "activationEvents": [ 24 | "onFileSystem:kube", 25 | "onView:kubernator.treeView", 26 | "onCommand:kubernator.create", 27 | "onCommand:kubernator.clean", 28 | "onCommand:kubernator.delete", 29 | "onCommand:kubernator.reconfigure" 30 | ], 31 | "main": "./out/extension.js", 32 | "contributes": { 33 | "viewsContainers": { 34 | "activitybar": [ 35 | { 36 | "id": "kubernator", 37 | "title": "Kubernator", 38 | "icon": "assets/kubernator.svg" 39 | } 40 | ] 41 | }, 42 | "views": { 43 | "kubernator": [ 44 | { 45 | "id": "kubernator.treeView", 46 | "name": "Kubernator" 47 | } 48 | ] 49 | }, 50 | "commands": [ 51 | { 52 | "command": "kubernator.refresh", 53 | "title": "Refresh", 54 | "icon": "$(refresh)", 55 | "category": "Kubernator" 56 | }, 57 | { 58 | "command": "kubernator.delete", 59 | "title": "Delete", 60 | "icon": "$(trash)", 61 | "category": "Kubernator" 62 | }, 63 | { 64 | "command": "kubernator.create", 65 | "title": "Create", 66 | "icon": "$(save)", 67 | "category": "Kubernator" 68 | }, 69 | { 70 | "command": "kubernator.clean", 71 | "title": "Clean", 72 | "category": "Kubernator" 73 | }, 74 | { 75 | "command": "kubernator.reconfigure", 76 | "title": "Reconfigure", 77 | "category": "Kubernator" 78 | }, 79 | { 80 | "command": "kubernator.gotoPV", 81 | "title": "Go to PV", 82 | "category": "Kubernator" 83 | }, 84 | { 85 | "command": "kubernator.reveal", 86 | "title": "Reveal", 87 | "category": "Kubernator" 88 | }, 89 | { 90 | "command": "kubernator.edit", 91 | "title": "Edit", 92 | "category": "Kubernator" 93 | }, 94 | { 95 | "command": "kubernator.shell", 96 | "title": "Shell", 97 | "category": "Kubernator" 98 | }, 99 | { 100 | "command": "kubernator.switchContext", 101 | "title": "Switch context", 102 | "category": "Kubernator" 103 | } 104 | ], 105 | "menus": { 106 | "commandPalette": [ 107 | { 108 | "command": "kubernator.create", 109 | "when": "editorLangId == yaml" 110 | }, 111 | { 112 | "command": "kubernator.clean", 113 | "when": "editorLangId == yaml" 114 | }, 115 | { 116 | "command": "kubernator.delete", 117 | "when": "editorLangId == yaml" 118 | }, 119 | { 120 | "command": "kubernator.reveal", 121 | "when": "editorLangId == yaml" 122 | }, 123 | { 124 | "command": "kubernator.refresh", 125 | "when": "false" 126 | }, 127 | { 128 | "command": "kubernator.gotoPV", 129 | "when": "false" 130 | }, 131 | { 132 | "command": "kubernator.edit", 133 | "when": "false" 134 | }, 135 | { 136 | "command": "kubernator.shell", 137 | "when": "false" 138 | } 139 | ], 140 | "view/title": [ 141 | { 142 | "command": "kubernator.refresh", 143 | "when": "view == kubernator.treeView", 144 | "group": "navigation" 145 | } 146 | ], 147 | "view/item/context": [ 148 | { 149 | "command": "kubernator.refresh", 150 | "when": "view == kubernator.treeView && viewItem =~ /\\bfolder\\b/", 151 | "group": "inline" 152 | }, 153 | { 154 | "command": "kubernator.delete", 155 | "when": "view == kubernator.treeView && viewItem =~ /\\bobject\\b/" 156 | }, 157 | { 158 | "command": "kubernator.gotoPV", 159 | "when": "view == kubernator.treeView && viewItem =~ /\\bobject:PersistentVolumeClaim\\b/" 160 | }, 161 | { 162 | "command": "kubernator.edit", 163 | "when": "view == kubernator.treeView && viewItem =~ /\\bobject\\b|\\bnamespace\\b/" 164 | }, 165 | { 166 | "command": "kubernator.shell", 167 | "when": "view == kubernator.treeView && viewItem =~ /\\bobject:Pod\\b/" 168 | } 169 | ] 170 | }, 171 | "configuration": [ 172 | { 173 | "title": "Kubernator", 174 | "properties": { 175 | "kubernator.apiURL": { 176 | "type": "string", 177 | "default": "", 178 | "description": "Base URL of Kubernetes API (will run kubectl proxy if not set)" 179 | }, 180 | "kubernator.excludeEmpty": { 181 | "type": "boolean", 182 | "default": true, 183 | "description": "don't show empty \"folders\" in tree viewlet" 184 | }, 185 | "kubernator.expandCoreGroup": { 186 | "type": "boolean", 187 | "default": true, 188 | "description": "automatically expand `[core]` \"folder\"" 189 | }, 190 | "kubernator.expandUndottedGroups": { 191 | "type": "boolean", 192 | "default": true, 193 | "description": "automatically expand \"folders\" without dots in their name" 194 | }, 195 | "kubernator.showManagedFields": { 196 | "type": "boolean", 197 | "default": false, 198 | "description": "disable stripping of object.metadata.managedFields" 199 | }, 200 | "kubernator.stripKubectlLastAppliedConfiguration": { 201 | "type": "boolean", 202 | "default": true, 203 | "description": "strip `kubectl.kubernetes.io/last-applied-configuration` annotation" 204 | }, 205 | "kubernator.decodeSecrets": { 206 | "type": "boolean", 207 | "default": true, 208 | "description": "automatically encode/decode v1.Secret.data to/from base64" 209 | } 210 | } 211 | } 212 | ] 213 | }, 214 | "scripts": { 215 | "vscode:prepublish": "npm run compile", 216 | "package": "npx vsce package", 217 | "publish": "npx vsce publish", 218 | "compile": "tsc -p ./", 219 | "watch": "tsc -watch -p ./", 220 | "lint": "eslint src --ext ts" 221 | }, 222 | "devDependencies": { 223 | "@types/glob": "^7.2.0", 224 | "@types/node": "14.x", 225 | "@types/node-fetch": "^2.6.1", 226 | "@types/vscode": "^1.66.0", 227 | "@typescript-eslint/eslint-plugin": "^5.16.0", 228 | "@typescript-eslint/parser": "^5.16.0", 229 | "eslint": "^8.11.0", 230 | "glob": "^7.2.0", 231 | "typescript": "^4.5.5", 232 | "vsce": "latest" 233 | }, 234 | "dependencies": { 235 | "@smpio/kube": "^0.11.0" 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/FSProvider.ts: -------------------------------------------------------------------------------- 1 | import * as nodePath from 'path'; 2 | import * as vscode from 'vscode'; 3 | import * as kube from './kube'; 4 | import * as interfaces from './interfaces'; 5 | 6 | const EXT_MIMETYPE_MAP: {[ext: string]: string} = { 7 | '.yaml': 'application/yaml', // eslint-disable-line @typescript-eslint/naming-convention 8 | '.json': 'application/json', // eslint-disable-line @typescript-eslint/naming-convention 9 | }; 10 | 11 | // TODO: add fetch cache for sequence "stat, read" 12 | export class FSProvider implements vscode.FileSystemProvider { 13 | static scheme = interfaces.DOCUMENT_SCHEME; 14 | 15 | private forceReloadFiles = new Set(); 16 | 17 | private onDidChangeFileEmitter: vscode.EventEmitter = new vscode.EventEmitter(); 18 | onDidChangeFile: vscode.Event = this.onDidChangeFileEmitter.event; 19 | 20 | watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable { 21 | return new vscode.Disposable(() => {}); 22 | } 23 | 24 | async stat(uri: vscode.Uri): Promise { 25 | if (this.forceReloadFiles.delete(uri.path)) { 26 | setTimeout(() => { 27 | this.onDidChangeFileEmitter.fire([{ 28 | type: vscode.FileChangeType.Changed, 29 | uri: uri, 30 | }]); 31 | }, 0); 32 | 33 | return { 34 | type: vscode.FileType.File, 35 | ctime: 0, 36 | mtime: 0, 37 | size: 65536, 38 | }; 39 | } 40 | 41 | let {path} = explodeUri(uri); 42 | let obj = await kube.api.fetch(path).then(r => r.json()) as any; 43 | 44 | let ctimeIso = obj.metadata?.creationTimestamp; 45 | let ctime = ctimeIso ? new Date(ctimeIso).getTime() : 0; 46 | let generation = obj.metadata?.generation; 47 | let mtime = generation ? ctime + generation : new Date().getTime(); 48 | 49 | return { 50 | type: vscode.FileType.File, 51 | ctime: ctime, 52 | mtime: mtime, 53 | size: 65536, 54 | }; 55 | } 56 | 57 | readDirectory(uri: vscode.Uri): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]> { 58 | return []; 59 | } 60 | 61 | createDirectory(uri: vscode.Uri): void | Thenable { 62 | // noop 63 | } 64 | 65 | async readFile(uri: vscode.Uri): Promise { 66 | let {path, mimetype} = explodeUri(uri); 67 | 68 | if (mimetype === 'application/yaml') { 69 | let config = vscode.workspace.getConfiguration('kubernator'); 70 | let obj = await kube.api.fetch(path).then(r => r.json()) as any; 71 | 72 | if (!config.showManagedFields && obj.metadata) { 73 | delete obj.metadata.managedFields; 74 | } 75 | 76 | if (config.stripKubectlLastAppliedConfiguration && obj.metadata?.annotations) { 77 | delete obj.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration']; 78 | if (Object.keys(obj.metadata.annotations).length === 0) { 79 | delete obj.metadata.annotations; 80 | } 81 | } 82 | 83 | let text = kube.yaml.stringify(obj, { 84 | decodeSecrets: config.decodeSecrets 85 | }); 86 | return Buffer.from(text); 87 | } 88 | 89 | return kube.api.fetch(path, mimetype).then(r => r.buffer()); 90 | } 91 | 92 | async writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): Promise { 93 | if (!vscode.window.state.focused) { 94 | // prevent autosave, see https://github.com/Microsoft/vscode/issues/42170 95 | throw new Error('Not saving file without focus!'); 96 | } 97 | if (vscode.window.activeTextEditor?.document.uri.toString() !== uri.toString()) { 98 | // prevent saving inactive file 99 | throw new Error('Not saving file without focus!'); 100 | } 101 | 102 | // we could use raw content with kube.api.put, 103 | // but we need to apply some manifest preprocessing first 104 | let obj = kube.yaml.parse(content.toString()); 105 | 106 | let {path} = explodeUri(uri); 107 | await kube.api.put(path, JSON.stringify(obj), 'application/json'); 108 | this.forceReloadFiles.add(uri.path); 109 | } 110 | 111 | async delete(uri: vscode.Uri, options: { recursive: boolean; }): Promise { 112 | let {path} = explodeUri(uri); 113 | await kube.api.delete(path); 114 | } 115 | 116 | rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Thenable { 117 | throw new Error('Not implemented.'); 118 | } 119 | } 120 | 121 | function explodeUri(uri: vscode.Uri): {path: string, mimetype?: string} { 122 | let path = uri.path; 123 | let ext = nodePath.extname(path); 124 | let mimetype = EXT_MIMETYPE_MAP[ext]; 125 | 126 | if (mimetype === undefined) { 127 | throw Error(`Unknown extension "${ext}"`); 128 | } 129 | 130 | path = path.slice(0, -ext.length); 131 | return {path, mimetype}; 132 | } 133 | -------------------------------------------------------------------------------- /src/TreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from './kube'; 3 | import { ttlCache, objectUri, ttlCacheClear } from './util'; 4 | 5 | const GLOBAL_PSEUDO_NAMESPACE = '[global]'; 6 | const CORE_API_GROUP_NAME = '[core]'; 7 | const CACHE_TTL_MS = 5000; 8 | 9 | export class TreeDataProvider implements vscode.TreeDataProvider { 10 | private root = new RootNode(); 11 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 12 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 13 | 14 | getTreeItem(element: Node) { 15 | return element; 16 | } 17 | 18 | async getChildren(element?: Node) { 19 | if (!kube.api.ready) { 20 | return []; 21 | } 22 | 23 | if (element) { 24 | return element.getChildren(); 25 | } else { 26 | return this.root.getChildren(); 27 | } 28 | } 29 | 30 | async getParent(element: Node) { 31 | return element.getParent(); 32 | } 33 | 34 | invalidate(element?: Node, {keepCache = false}: {keepCache: boolean} = {keepCache: false}) { 35 | if (!keepCache) { 36 | if (!element) { 37 | this.root.invalidate(); 38 | } else { 39 | element.invalidate(); 40 | } 41 | } 42 | 43 | this._onDidChangeTreeData.fire(element); 44 | } 45 | } 46 | 47 | export abstract class Node extends vscode.TreeItem { 48 | abstract getChildren(): vscode.ProviderResult; 49 | abstract getParent(): vscode.ProviderResult; 50 | 51 | invalidate(): void { 52 | ttlCacheClear(this, 'getChildren'); 53 | } 54 | } 55 | 56 | class RootNode extends Node { 57 | id = 'root'; 58 | 59 | constructor() { 60 | super('', vscode.TreeItemCollapsibleState.Expanded); 61 | } 62 | 63 | @ttlCache(CACHE_TTL_MS) 64 | async getChildren() { 65 | let namespaces = await kube.api.list(kube.api.groups[''].bestVersion.resourcesByKind.Namespace); 66 | return [undefined, ...namespaces].map(ns => new NamespaceNode(ns?.metadata.name)); 67 | } 68 | 69 | getParent() { 70 | return null; 71 | } 72 | } 73 | 74 | class NamespaceNode extends Node { 75 | public ns?: string; 76 | 77 | constructor(ns?: string) { 78 | let label = ns ?? GLOBAL_PSEUDO_NAMESPACE; 79 | super(label, vscode.TreeItemCollapsibleState.Collapsed); 80 | this.contextValue = ns ? 'folder namespace' : 'folder'; 81 | this.ns = ns; 82 | this.id = nodeID.namespace(ns); 83 | 84 | if (this.ns) { 85 | this.resourceUri = objectUri({ 86 | apiVersion: 'v1', 87 | kind: 'Namespace', 88 | metadata: { 89 | name: this.ns, 90 | }, 91 | }); 92 | } 93 | } 94 | 95 | @ttlCache(CACHE_TTL_MS) 96 | async getChildren() { 97 | let config = vscode.workspace.getConfiguration('kubernator'); 98 | 99 | let groups = Object.values(kube.api.groups).sort((a, b) => { 100 | let name1 = a.name; 101 | let name2 = b.name; 102 | if (name1.indexOf('.') === -1) { 103 | name1 = '_' + name1; 104 | } 105 | if (name2.indexOf('.') === -1) { 106 | name2 = '_' + name2; 107 | } 108 | return name1.localeCompare(name2); 109 | }); 110 | let children = groups.map(g => new GroupNode(g, this.ns)); 111 | 112 | if (config.excludeEmpty) { 113 | children = await excludeEmpty(children); 114 | } 115 | return children; 116 | } 117 | 118 | getParent() { 119 | return null; 120 | } 121 | } 122 | 123 | export class GroupNode extends Node { 124 | public group: kube.Group; 125 | public ns?: string; 126 | 127 | constructor(group: kube.Group, ns?: string) { 128 | let config = vscode.workspace.getConfiguration('kubernator'); 129 | 130 | let collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 131 | if (config.expandCoreGroup && group.name === '' || 132 | config.expandUndottedGroups && group.name.indexOf('.') === -1) { 133 | collapsibleState = vscode.TreeItemCollapsibleState.Expanded; 134 | } 135 | 136 | let label = group.name === '' ? CORE_API_GROUP_NAME : group.name; 137 | 138 | super(label, collapsibleState); 139 | this.contextValue = 'folder group'; 140 | this.group = group; 141 | this.ns = ns; 142 | this.id = nodeID.group(group, ns); 143 | } 144 | 145 | @ttlCache(CACHE_TTL_MS) 146 | async getChildren() { 147 | let config = vscode.workspace.getConfiguration('kubernator'); 148 | 149 | let resourceDoesMatch = (r: kube.Resource) => { 150 | if (r.verbs.indexOf('list') === -1) { 151 | return false; 152 | } 153 | if (r.verbs.indexOf('get') === -1) { 154 | return false; 155 | } 156 | 157 | return !!this.ns === r.namespaced; 158 | }; 159 | 160 | let resources = Object.values(this.group.bestVersion.resourcesByKind).filter(resourceDoesMatch); 161 | let children = resources.map(r => new ResourceNode(r, this.ns)); 162 | 163 | if (config.excludeEmpty) { 164 | children = await excludeEmpty(children); 165 | } 166 | return children; 167 | } 168 | 169 | getParent() { 170 | return new NamespaceNode(this.ns); 171 | } 172 | } 173 | 174 | export class ResourceNode extends Node { 175 | constructor(public resource: kube.Resource, public ns?: string) { 176 | super(resource.kind, vscode.TreeItemCollapsibleState.Collapsed); 177 | this.contextValue = 'folder resource'; 178 | this.id = nodeID.resource(resource, ns); 179 | } 180 | 181 | @ttlCache(CACHE_TTL_MS) 182 | async getChildren() { 183 | try { 184 | let objects = await kube.api.list(this.resource, this.ns); 185 | return objects.map(obj => new ObjectNode(obj)); 186 | } catch(err) { 187 | if (err instanceof kube.APIError) { 188 | return [new ErrorNode(err)]; 189 | } else { 190 | throw err; 191 | } 192 | } 193 | } 194 | 195 | getParent() { 196 | return new GroupNode(this.resource.groupVersion.group, this.ns); 197 | } 198 | } 199 | 200 | export class ObjectNode extends Node { 201 | resourceUri: vscode.Uri; 202 | 203 | constructor(public obj: kube.Object) { 204 | super(obj.metadata.name, vscode.TreeItemCollapsibleState.None); 205 | this.contextValue = `leaf object:${obj.kind}`; 206 | this.resourceUri = objectUri(obj); 207 | this.command = { 208 | title: 'open', 209 | command: 'vscode.open', 210 | arguments: [this.resourceUri], 211 | }; 212 | this.id = nodeID.object(obj); 213 | 214 | let decorator = getObjectDecorator(obj); 215 | let color = decorator?.color && new vscode.ThemeColor(decorator.color); 216 | this.description = decorator?.text; 217 | this.iconPath = new vscode.ThemeIcon('debug-breakpoint-data-unverified', color); 218 | } 219 | 220 | getChildren() { 221 | return []; 222 | } 223 | 224 | getParent() { 225 | return new ResourceNode(kube.api.getResource(this.obj), this.obj.metadata.namespace); 226 | } 227 | } 228 | 229 | class ErrorNode extends Node { 230 | constructor(public readonly err: Error) { 231 | super('Error: ' + err.message, vscode.TreeItemCollapsibleState.None); 232 | this.tooltip = err.message; 233 | this.contextValue = 'leaf error'; 234 | this.iconPath = new vscode.ThemeIcon('error', new vscode.ThemeColor('errorForeground')); 235 | } 236 | 237 | getChildren() { 238 | return []; 239 | } 240 | 241 | getParent() { 242 | return null; 243 | } 244 | } 245 | 246 | async function excludeEmpty(nodes: N[]) { 247 | return asyncFilter(nodes, async node => { 248 | let children = await Promise.resolve(node.getChildren()); 249 | if (!children) { 250 | return false; 251 | } 252 | return children.some(child => child.contextValue !== 'error'); 253 | }); 254 | } 255 | 256 | async function asyncFilter(arr: T[], predicate: (value: T) => Promise) { 257 | const results = await Promise.all(arr.map(predicate)); 258 | return arr.filter((_, index) => results[index]); 259 | } 260 | 261 | const nodeID = { 262 | namespace: (ns?: string) => ns ?? 'GLOBAL', 263 | group: (group: kube.Group, ns?: string) => nodeID.namespace(ns) + ':' + group.name, 264 | resource: (resource: kube.Resource, ns?: string) => nodeID.group(resource.groupVersion.group, ns) + ':' + resource.kind, 265 | object: (obj: kube.Object) => nodeID.resource(kube.api.getResource(obj), obj.metadata.namespace) + ':' + obj.metadata.name, 266 | }; 267 | 268 | function getObjectDecorator (obj: any): ObjectDecorator|undefined { 269 | let s = obj.status; 270 | if (s) { 271 | let replicas = s.replicas ?? s.desiredNumberScheduled; 272 | let ready = s.readyReplicas ?? s.numberReady; 273 | 274 | if (replicas !== undefined) { 275 | if (ready !== undefined) { 276 | return { 277 | text: `${ready}/${replicas}`, 278 | color: ready === replicas ? 'notebookStatusSuccessIcon.foreground' : 'notebookStatusRunningIcon.foreground', 279 | }; 280 | } else { 281 | return {text: `${replicas}`}; 282 | } 283 | } 284 | 285 | if (s.succeeded !== undefined) { 286 | if (s.succeeded) { 287 | return decorators.success; 288 | } else { 289 | return { 290 | text: '✗', 291 | color: 'notebookStatusErrorIcon.foreground', 292 | }; 293 | } 294 | } 295 | 296 | if (s.containerStatuses !== undefined) { 297 | let reasons = s.containerStatuses.map((s: any) => s.state.terminated?.reason ?? s.state.waiting?.reason); 298 | let reason = reasons.find((r: any) => !!r); 299 | if (reason) { 300 | return { 301 | text: reason, 302 | color: 'notebookStatusErrorIcon.foreground', 303 | }; 304 | } 305 | } 306 | 307 | if (s.phase !== undefined) { 308 | if (s.phase === 'Bound' || s.phase === 'Succeeded' || s.phase === 'Active') { 309 | return decorators.success; 310 | } else { 311 | return { 312 | text: s.phase, 313 | color: 'notebookStatusRunningIcon.foreground', 314 | }; 315 | } 316 | } 317 | } 318 | } 319 | 320 | interface ObjectDecorator { 321 | text?: string; 322 | color?: string; 323 | } 324 | 325 | const decorators = { 326 | success: { 327 | text: '✓', 328 | color: 'notebookStatusSuccessIcon.foreground', 329 | }, 330 | }; 331 | -------------------------------------------------------------------------------- /src/commands/clean.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from '../kube'; 3 | 4 | 5 | export async function cleanObjectInActiveEditor() { 6 | let config = vscode.workspace.getConfiguration('kubernator'); 7 | let editor = vscode.window.activeTextEditor; 8 | if (!editor) { 9 | return; 10 | } 11 | 12 | let document = editor.document; 13 | let text = document.getText(); 14 | let obj = kube.yaml.parse(text); 15 | 16 | if (typeof obj !== 'object') { 17 | return; 18 | } 19 | 20 | kube.cleanObject(obj, kube.api); 21 | 22 | text = kube.yaml.stringify(obj, { 23 | decodeSecrets: config.decodeSecrets 24 | }); 25 | 26 | if (document.isUntitled) { 27 | let all = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(document.lineCount, 0)); 28 | editor.edit(editBuilder => { 29 | editBuilder.replace(all, text); 30 | }); 31 | } else { 32 | document = await vscode.workspace.openTextDocument({ 33 | content: text, 34 | language: document.languageId, 35 | }); 36 | vscode.window.showTextDocument(document); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/create.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from '../kube'; 3 | import { objectUri, closeActiveEditor } from '../util'; 4 | 5 | export async function createObjectFromActiveEditor() { 6 | let editor = vscode.window.activeTextEditor; 7 | if (!editor) { 8 | return; 9 | } 10 | 11 | let document = editor.document; 12 | let text = document.getText(); 13 | let obj = kube.yaml.parse(text); 14 | 15 | if (!obj.apiVersion) { 16 | throw new Error('Invalid YAML: apiVersion not specified'); 17 | } 18 | 19 | if (!obj.kind) { 20 | throw new Error('Invalid YAML: kind not specified'); 21 | } 22 | 23 | let resource = kube.api.getResource(obj.apiVersion, obj.kind); 24 | let postUri = kube.api.getResourceUri(resource, obj.metadata?.namespace); 25 | 26 | obj = await kube.api.post(postUri, JSON.stringify(obj), 'application/json').then(r => r.json()); 27 | 28 | closeActiveEditor(); 29 | vscode.window.showTextDocument(objectUri(obj), { preview: false }); 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/delete.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from '../kube'; 3 | 4 | export async function deleteObjectFromActiveEditor() { 5 | let editor = vscode.window.activeTextEditor; 6 | if (!editor) { 7 | return; 8 | } 9 | 10 | let document = editor.document; 11 | let text = document.getText(); 12 | let obj = kube.yaml.parse(text); 13 | 14 | await kube.api.delete(kube.api.getObjectUri(obj)); 15 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/reveal.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from '../kube'; 3 | import { Node, ObjectNode } from '../TreeDataProvider'; 4 | 5 | export function revealObjectInActiveEditor(treeView: vscode.TreeView) { 6 | let editor = vscode.window.activeTextEditor; 7 | if (!editor) { 8 | return; 9 | } 10 | 11 | let document = editor.document; 12 | let text = document.getText(); 13 | let obj = kube.yaml.parse(text); 14 | 15 | if (typeof obj !== 'object') { 16 | return; 17 | } 18 | 19 | if (!obj.metadata) { 20 | throw Error('No object metadata'); 21 | } 22 | 23 | treeView.reveal(new ObjectNode(obj), { 24 | focus: true, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/shell.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ObjectNode } from '../TreeDataProvider'; 3 | 4 | export async function startShell (node: ObjectNode) { 5 | let shellsToTry = ['sh']; 6 | let pod = node.obj as any; 7 | 8 | if (pod.spec?.containers?.length > 1) { 9 | let selected = await vscode.window.showQuickPick(pod.spec.containers.map((c: any) => c.name)); 10 | 11 | if (!selected) { 12 | return; 13 | } 14 | 15 | runExec(shellsToTry, selected); 16 | } else { 17 | runExec(shellsToTry); 18 | } 19 | 20 | function runExec(shells: string[], container?: string) { 21 | const terminal = vscode.window.createTerminal(pod.metadata.name); 22 | 23 | let shell = shells[0]; 24 | 25 | let containerOpts = ''; 26 | if (container) { 27 | containerOpts = '-c ' + container; 28 | } 29 | 30 | terminal.sendText(`exec kubectl -n ${pod.metadata.namespace} exec -it ${pod.metadata.name} ${containerOpts} -- ${shell}`); 31 | terminal.show(); 32 | 33 | if (shells.length > 1) { 34 | let closeHandler = vscode.window.onDidCloseTerminal(t => { 35 | if (t !== terminal) { 36 | return; 37 | } 38 | 39 | if (t.exitStatus?.code === 126) { 40 | runExec(shells.slice(1)); 41 | } 42 | closeHandler.dispose(); 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as kube from './kube'; 3 | import { TreeDataProvider, Node, ObjectNode } from './TreeDataProvider'; 4 | import { FSProvider } from './FSProvider'; 5 | import { createObjectFromActiveEditor } from './commands/create'; 6 | import { cleanObjectInActiveEditor } from './commands/clean'; 7 | import { deleteObjectFromActiveEditor } from './commands/delete'; 8 | import { revealObjectInActiveEditor } from './commands/reveal'; 9 | import { startShell } from './commands/shell'; 10 | 11 | const switchContextCommandId = 'kubernator.switchContext'; 12 | 13 | export async function activate(context: vscode.ExtensionContext) { 14 | function d(disposable: T): T { 15 | context.subscriptions.push(disposable); 16 | return disposable; 17 | } 18 | 19 | let proxy: kube.ProxyWithContext|null = null; 20 | let statusBarItem = d(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left)); 21 | let fsProvider = new FSProvider(); 22 | let treeDataProvider = new TreeDataProvider(); 23 | let treeView = d(vscode.window.createTreeView('kubernator.treeView', { 24 | treeDataProvider: treeDataProvider, 25 | showCollapseAll: true, 26 | })); 27 | 28 | d(vscode.workspace.registerFileSystemProvider(FSProvider.scheme, fsProvider, { 29 | isCaseSensitive: true, 30 | })); 31 | 32 | statusBarItem.command = switchContextCommandId; 33 | statusBarItem.show(); 34 | 35 | async function reconfigure(ctx?: string) { 36 | let config = vscode.workspace.getConfiguration('kubernator'); 37 | statusBarItem.text = 'Kubernating...'; 38 | 39 | if (!ctx && config.apiURL) { 40 | await kube.api.configure({apiURL: config.apiURL}); 41 | statusBarItem.text = config.apiURL; 42 | } else { 43 | if (proxy && ctx && proxy.context !== ctx) { 44 | proxy.dispose(); 45 | proxy = null; 46 | } 47 | 48 | if (!ctx) { 49 | if (proxy) { 50 | ctx = proxy.context; 51 | } else { 52 | ctx = await kube.getDefaultContext(); 53 | } 54 | } 55 | 56 | if (!proxy) { 57 | proxy = await kube.startProxy(ctx); 58 | d(proxy); 59 | } 60 | 61 | await kube.api.configure({socketPath: proxy.socketPath}); 62 | statusBarItem.text = ctx; 63 | } 64 | 65 | treeDataProvider.invalidate(); 66 | } 67 | 68 | await reconfigure(); 69 | 70 | d(vscode.workspace.onDidChangeConfiguration(e => { 71 | if (e.affectsConfiguration('kubernator')) { 72 | reconfigure(proxy?.context); 73 | } 74 | })); 75 | 76 | d(vscode.commands.registerCommand('kubernator.reconfigure', reconfigure)); 77 | 78 | d(vscode.commands.registerCommand('kubernator.refresh', (node?: Node) => { 79 | treeDataProvider.invalidate(node); 80 | })); 81 | 82 | // Workaround for TreeView content caching 83 | // By default, once tree view item has been expanded, its children are saved in cache that is not cleared 84 | // after collapsing the item. 85 | // Another approach would be calling invalidate immediately after expand, but this will lead to 86 | // race conditions. See 06f58be for details. 87 | d(treeView.onDidExpandElement(e => { 88 | setTimeout(() => { 89 | treeDataProvider.invalidate(e.element, {keepCache: true}); 90 | }, 1000); 91 | })); 92 | 93 | d(vscode.commands.registerCommand('kubernator.delete', handleErrors(async (node?: ObjectNode) => { 94 | if (node) { 95 | await fsProvider.delete(node.resourceUri, {recursive: false}); 96 | treeDataProvider.invalidate(node.getParent()); 97 | } else { 98 | deleteObjectFromActiveEditor(); 99 | } 100 | }))); 101 | 102 | d(vscode.commands.registerCommand('kubernator.create', handleErrors(createObjectFromActiveEditor))); 103 | d(vscode.commands.registerCommand('kubernator.clean', handleErrors(cleanObjectInActiveEditor))); 104 | 105 | d(vscode.commands.registerCommand('kubernator.gotoPV', handleErrors(async (node: ObjectNode) => { 106 | let obj = node.obj as any; 107 | let pvName = obj.spec.volumeName; 108 | if (!pvName) { 109 | throw new Error('volumeName not set'); 110 | } 111 | 112 | treeView.reveal(new ObjectNode({ 113 | apiVersion: 'v1', 114 | kind: 'PersistentVolume', 115 | metadata: { 116 | name: pvName, 117 | }, 118 | }), { 119 | focus: true, 120 | }); 121 | }))); 122 | 123 | d(vscode.commands.registerCommand('kubernator.reveal', handleErrors(() => revealObjectInActiveEditor(treeView)))); 124 | 125 | d(vscode.commands.registerCommand('kubernator.edit', handleErrors( 126 | (node: ObjectNode) => vscode.commands.executeCommand('vscode.open', node.resourceUri)))); 127 | 128 | d(vscode.commands.registerCommand('kubernator.shell', handleErrors(startShell))); 129 | 130 | d(vscode.commands.registerCommand(switchContextCommandId, handleErrors(async () => { 131 | let config = vscode.workspace.getConfiguration('kubernator'); 132 | 133 | let items: ContextPickItem[] = (await kube.getContexts()).map(ctx => ({ 134 | label: ctx, 135 | })); 136 | if (config.apiURL) { 137 | items.push({ 138 | label: config.apiURL, 139 | isApiURL: true, 140 | description: 'Use API URL', 141 | }); 142 | } 143 | let quickPick = vscode.window.createQuickPick(); 144 | quickPick.items = items; 145 | quickPick.onDidChangeSelection(handleErrors(async selection => { 146 | if (!selection[0]) { 147 | return; 148 | } 149 | 150 | quickPick.hide(); 151 | statusBarItem.text = 'Kubernating...'; 152 | 153 | if (selection[0].isApiURL) { 154 | await reconfigure(); 155 | } else { 156 | await reconfigure(selection[0].label); 157 | } 158 | })); 159 | quickPick.onDidHide(() => quickPick.dispose()); 160 | quickPick.show(); 161 | }))); 162 | } 163 | 164 | export function deactivate() { 165 | } 166 | 167 | function handleErrors any>(fn: F): (...a: Parameters) => Promise> { 168 | return async function (this: any, ...a: Parameters) { 169 | try { 170 | let ret = await Promise.resolve(fn.apply(this, a)); 171 | return ret; 172 | } catch (err: any) { 173 | vscode.window.showErrorMessage(err.message); 174 | } 175 | }; 176 | } 177 | 178 | interface ContextPickItem extends vscode.QuickPickItem { 179 | isApiURL?: boolean; 180 | } 181 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export const DOCUMENT_SCHEME = 'kube'; 2 | -------------------------------------------------------------------------------- /src/kube.ts: -------------------------------------------------------------------------------- 1 | import {API} from '@smpio/kube'; 2 | export * from '@smpio/kube'; 3 | export const api = new API(); 4 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as interfaces from './interfaces'; 3 | import * as kube from './kube'; 4 | 5 | const CACHE_PROP = Symbol('ttlCache'); 6 | 7 | export function ttlCache(ttlMs: number) { 8 | return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { 9 | let method = descriptor.value!; 10 | 11 | descriptor.value = function cached () { 12 | let cacheStore = (this as any)[CACHE_PROP]; 13 | if (!cacheStore) { 14 | cacheStore = (this as any)[CACHE_PROP] = {}; 15 | } 16 | let cache = cacheStore[propertyName]; 17 | 18 | if (!cache || Date.now() - cache.time > ttlMs) { 19 | cache = cacheStore[propertyName] = { 20 | value: method.apply(this, arguments), 21 | time: Date.now(), 22 | }; 23 | } 24 | return cache.value; 25 | }; 26 | }; 27 | } 28 | 29 | export function ttlCacheClear(target: any, propertyName: string) { 30 | let cacheStore = target[CACHE_PROP]; 31 | if (cacheStore) { 32 | delete cacheStore[propertyName]; 33 | } 34 | } 35 | 36 | export function objectUri(obj: kube.Object) { 37 | let path = kube.api.getObjectUri(obj); 38 | return vscode.Uri.parse(`${interfaces.DOCUMENT_SCHEME}:/${path}.yaml`); 39 | } 40 | 41 | export function closeActiveEditor() { 42 | let editor = vscode.window.activeTextEditor; 43 | if (!editor) { 44 | return; 45 | } 46 | 47 | if (editor.document.isUntitled) { 48 | let all = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(editor.document.lineCount, 0)); 49 | editor.edit(editBuilder => { 50 | editBuilder.replace(all, ''); 51 | }); 52 | } 53 | 54 | vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------