├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------