mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
cleanup Lens repo with tighter linting
Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
parent
e468105143
commit
b1ff34879a
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dashboard/node_modules
|
||||||
76
.eslintrc.js
76
.eslintrc.js
@ -1,76 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
"src/renderer/**/*.js",
|
|
||||||
"build/**/*.js",
|
|
||||||
"src/renderer/**/*.vue"
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:vue/recommended'
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"indent": ["error", 2],
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"vue/order-in-components": "off",
|
|
||||||
"vue/attributes-order": "off",
|
|
||||||
"vue/max-attributes-per-line": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
"build/*.ts",
|
|
||||||
"src/**/*.ts",
|
|
||||||
"spec/**/*.ts"
|
|
||||||
],
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"indent": ["error", 2]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
"dashboard/**/*.ts",
|
|
||||||
"dashboard/**/*.tsx",
|
|
||||||
],
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/interface-name-prefix": "off",
|
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/ban-ts-ignore": "off",
|
|
||||||
"indent": ["error", 2]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
96
.eslintrc.json
Normal file
96
.eslintrc.json
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "16.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/renderer/**/*.js",
|
||||||
|
"build/**/*.js",
|
||||||
|
"src/renderer/**/*.vue"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:vue/recommended"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"vue/order-in-components": "off",
|
||||||
|
"vue/attributes-order": "off",
|
||||||
|
"vue/max-attributes-per-line": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"build/*.ts",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"spec/**/*.ts",
|
||||||
|
"dashboard/**/*.ts",
|
||||||
|
"dashboard/**/*.tsx"
|
||||||
|
],
|
||||||
|
"excludedFiles": [
|
||||||
|
"**/node_modules/**/*"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "error",
|
||||||
|
"@typescript-eslint/array-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"default": "array",
|
||||||
|
"readyonly": "array"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"@typescript-eslint/no-use-before-define": "error",
|
||||||
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "error",
|
||||||
|
"@typescript-eslint/await-thenable": "error",
|
||||||
|
"@typescript-eslint/consistent-type-assertions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"assertionStyle": "as",
|
||||||
|
"objectLiteralTypeAssertions": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-definitions": [
|
||||||
|
"error",
|
||||||
|
"interface"
|
||||||
|
],
|
||||||
|
"react/prop-types": [ 0 ],
|
||||||
|
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
|
||||||
|
"curly": "error",
|
||||||
|
"space-before-blocks": "error",
|
||||||
|
"semi": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
Makefile
5
Makefile
@ -27,7 +27,10 @@ integration-win:
|
|||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
yarn lint
|
yarn run eslint --ext tsx,ts,vue --max-warnings=0 src/ dashboard/ build/
|
||||||
|
|
||||||
|
lint-fix:
|
||||||
|
yarn run eslint --ext tsx,ts,vue --max-warnings=0 src/ dashboard/ build/ --fix
|
||||||
|
|
||||||
test-app:
|
test-app:
|
||||||
yarn test
|
yarn test
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { helmCli } from "../src/main/helm-cli"
|
import { helmCli } from "../src/main/helm-cli";
|
||||||
|
|
||||||
helmCli.ensureBinary()
|
helmCli.ensureBinary();
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import * as request from "request"
|
import * as request from "request";
|
||||||
import * as fs from "fs"
|
import * as fs from "fs";
|
||||||
import { ensureDir, pathExists } from "fs-extra"
|
import { ensureDir, pathExists } from "fs-extra";
|
||||||
import * as md5File from "md5-file"
|
import * as md5File from "md5-file";
|
||||||
import * as requestPromise from "request-promise-native"
|
import * as requestPromise from "request-promise-native";
|
||||||
import * as path from "path"
|
import * as path from "path";
|
||||||
|
|
||||||
class KubectlDownloader {
|
class KubectlDownloader {
|
||||||
public kubectlVersion: string
|
public kubectlVersion: string
|
||||||
@ -13,92 +13,94 @@ class KubectlDownloader {
|
|||||||
|
|
||||||
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
||||||
this.kubectlVersion = clusterVersion;
|
this.kubectlVersion = clusterVersion;
|
||||||
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl"
|
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
||||||
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
||||||
this.dirname = path.dirname(target);
|
this.dirname = path.dirname(target);
|
||||||
this.path = target;
|
this.path = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async urlEtag() {
|
protected async urlEtag(): Promise<string> {
|
||||||
const response = await requestPromise({
|
const response = await requestPromise({
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
uri: this.url,
|
uri: this.url,
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
}).catch((error) => { console.log(error) })
|
}).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
|
||||||
if (response.headers["etag"]) {
|
if (response.headers["etag"]) {
|
||||||
return response.headers["etag"].replace(/"/g, "")
|
return response.headers["etag"].replace(/"/g, "");
|
||||||
}
|
}
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkBinary() {
|
public async checkBinary(): Promise<boolean> {
|
||||||
const exists = await pathExists(this.path)
|
const exists = await pathExists(this.path);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const hash = md5File.sync(this.path)
|
const hash = md5File.sync(this.path);
|
||||||
const etag = await this.urlEtag()
|
const etag = await this.urlEtag();
|
||||||
if(hash == etag) {
|
if(hash == etag) {
|
||||||
console.log("Kubectl md5sum matches the remote etag")
|
console.log("Kubectl md5sum matches the remote etag");
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again")
|
console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again");
|
||||||
await fs.promises.unlink(this.path)
|
await fs.promises.unlink(this.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadKubectl() {
|
public async downloadKubectl(): Promise<void> {
|
||||||
const exists = await this.checkBinary();
|
const exists = await this.checkBinary();
|
||||||
if(exists) {
|
if(exists) {
|
||||||
console.log("Already exists and is valid")
|
console.log("Already exists and is valid");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
await ensureDir(path.dirname(this.path), 0o755)
|
await ensureDir(path.dirname(this.path), 0o755);
|
||||||
|
|
||||||
const file = fs.createWriteStream(this.path)
|
const file = fs.createWriteStream(this.path);
|
||||||
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`)
|
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
const requestOpts: request.UriOptions & request.CoreOptions = {
|
const requestOpts: request.UriOptions & request.CoreOptions = {
|
||||||
uri: this.url,
|
uri: this.url,
|
||||||
gzip: true
|
gzip: true
|
||||||
}
|
};
|
||||||
const stream = request(requestOpts)
|
const stream = request(requestOpts);
|
||||||
|
|
||||||
stream.on("complete", () => {
|
stream.on("complete", () => {
|
||||||
console.log("kubectl binary download finished")
|
console.log("kubectl binary download finished");
|
||||||
file.end(() => {})
|
file.end(() => {});
|
||||||
})
|
});
|
||||||
|
|
||||||
stream.on("error", (error) => {
|
stream.on("error", (error) => {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
fs.unlink(this.path, () => {})
|
fs.unlink(this.path, () => {});
|
||||||
throw(error)
|
throw(error);
|
||||||
})
|
});
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
file.on("close", () => {
|
file.on("close", () => {
|
||||||
console.log("kubectl binary download closed")
|
console.log("kubectl binary download closed");
|
||||||
fs.chmod(this.path, 0o755, () => {})
|
fs.chmod(this.path, 0o755, () => {});
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
stream.pipe(file)
|
stream.pipe(file);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadVersion: string = require("../package.json").config.bundledKubectlVersion
|
const downloadVersion: string = require("../package.json").config.bundledKubectlVersion;
|
||||||
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client')
|
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client');
|
||||||
const downloads = [
|
const downloads = [
|
||||||
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
|
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
|
||||||
{ platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') },
|
{ platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') },
|
||||||
{ platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') },
|
{ platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') },
|
||||||
{ platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') }
|
{ platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') }
|
||||||
]
|
];
|
||||||
|
|
||||||
downloads.forEach((dlOpts) => {
|
downloads.forEach((dlOpts) => {
|
||||||
console.log(dlOpts)
|
console.log(dlOpts);
|
||||||
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
||||||
console.log("Downloading: " + JSON.stringify(dlOpts));
|
console.log("Downloading: " + JSON.stringify(dlOpts));
|
||||||
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")))
|
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { KubeApi, IKubeApiLinkBase } from "../kube-api";
|
import { KubeApi, KubeApiLinkBase } from "../kube-api";
|
||||||
|
|
||||||
interface ParseAPITest {
|
interface ParseAPITest {
|
||||||
url: string;
|
url: string;
|
||||||
expected: Required<IKubeApiLinkBase>;
|
expected: Required<KubeApiLinkBase>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tests: ParseAPITest[] = [
|
const tests: ParseAPITest[] = [
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class ApiManager {
|
|||||||
private stores = observable.map<KubeApi, KubeObjectStore>();
|
private stores = observable.map<KubeApi, KubeObjectStore>();
|
||||||
private views = observable.map<KubeApi, ApiComponents>();
|
private views = observable.map<KubeApi, ApiComponents>();
|
||||||
|
|
||||||
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)): KubeApi<any> {
|
||||||
if (typeof pathOrCallback === "string") {
|
if (typeof pathOrCallback === "string") {
|
||||||
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
|
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
|
||||||
}
|
}
|
||||||
@ -25,27 +25,32 @@ export class ApiManager {
|
|||||||
return Array.from(this.apis.values()).find(pathOrCallback);
|
return Array.from(this.apis.values()).find(pathOrCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerApi(apiBase: string, api: KubeApi) {
|
registerApi(apiBase: string, api: KubeApi): void {
|
||||||
if (!this.apis.has(apiBase)) {
|
if (!this.apis.has(apiBase)) {
|
||||||
this.apis.set(apiBase, api);
|
this.apis.set(apiBase, api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected resolveApi(api: string | KubeApi): KubeApi {
|
protected resolveApi(api: string | KubeApi): KubeApi {
|
||||||
if (typeof api === "string") return this.getApi(api)
|
if (typeof api === "string") {
|
||||||
|
return this.getApi(api);
|
||||||
|
}
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterApi(api: string | KubeApi) {
|
unregisterApi(api: string | KubeApi): void {
|
||||||
if (typeof api === "string") this.apis.delete(api);
|
if (typeof api === "string") {
|
||||||
else {
|
this.apis.delete(api);
|
||||||
|
} else {
|
||||||
const apis = Array.from(this.apis.entries());
|
const apis = Array.from(this.apis.entries());
|
||||||
const entry = apis.find(entry => entry[1] === api);
|
const entry = apis.find(entry => entry[1] === api);
|
||||||
if (entry) this.unregisterApi(entry[0]);
|
if (entry) {
|
||||||
|
this.unregisterApi(entry[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerStore(api: KubeApi, store: KubeObjectStore) {
|
registerStore(api: KubeApi, store: KubeObjectStore): void {
|
||||||
this.stores.set(api, store);
|
this.stores.set(api, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +58,7 @@ export class ApiManager {
|
|||||||
return this.stores.get(this.resolveApi(api));
|
return this.stores.get(this.resolveApi(api));
|
||||||
}
|
}
|
||||||
|
|
||||||
registerViews(api: KubeApi | KubeApi[], views: ApiComponents) {
|
registerViews(api: KubeApi | KubeApi[], views: ApiComponents): void {
|
||||||
if (Array.isArray(api)) {
|
if (Array.isArray(api)) {
|
||||||
api.forEach(api => this.registerViews(api, views));
|
api.forEach(api => this.registerViews(api, views));
|
||||||
return;
|
return;
|
||||||
@ -66,7 +71,7 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getViews(api: string | KubeApi): ApiComponents {
|
getViews(api: string | KubeApi): ApiComponents {
|
||||||
return this.views.get(this.resolveApi(api)) || {}
|
return this.views.get(this.resolveApi(api)) || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const cronJob = new CronJob({
|
|||||||
suspend: false,
|
suspend: false,
|
||||||
},
|
},
|
||||||
status: {}
|
status: {}
|
||||||
} as any)
|
} as any);
|
||||||
|
|
||||||
describe("Check for CronJob schedule never run", () => {
|
describe("Check for CronJob schedule never run", () => {
|
||||||
test("Should be false with normal schedule", () => {
|
test("Should be false with normal schedule", () => {
|
||||||
@ -31,12 +31,12 @@ describe("Check for CronJob schedule never run", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Should be true with date 31 of February", () => {
|
test("Should be true with date 31 of February", () => {
|
||||||
cronJob.spec.schedule = "30 06 31 2 *"
|
cronJob.spec.schedule = "30 06 31 2 *";
|
||||||
expect(cronJob.isNeverRun()).toBeTruthy();
|
expect(cronJob.isNeverRun()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should be true with date 32 of July", () => {
|
test("Should be true with date 32 of July", () => {
|
||||||
cronJob.spec.schedule = "0 30 06 32 7 *"
|
cronJob.spec.schedule = "0 30 06 32 7 *";
|
||||||
expect(cronJob.isNeverRun()).toBeTruthy();
|
expect(cronJob.isNeverRun()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,96 +3,23 @@
|
|||||||
// API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html
|
// API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html
|
||||||
|
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { ISecretRef, secretsApi } from "./secret.api";
|
import { SecretRef, secretsApi } from "./secret.api";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl } from "../../navigation";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class Certificate extends KubeObject {
|
export type IssuerType = "ACME" | "CA" | "SelfSigned" | "Vault" | "Venafi";
|
||||||
static kind = "Certificate"
|
|
||||||
|
|
||||||
spec: {
|
export interface IssuerConditionBase {
|
||||||
secretName: string;
|
lastTransitionTime: string; // 2019-06-05T07:10:42Z,
|
||||||
commonName?: string;
|
message: string; // The ACME account was registered with the ACME server,
|
||||||
dnsNames?: string[];
|
reason: string; // ACMEAccountRegistered,
|
||||||
organization?: string[];
|
status: string; // True,
|
||||||
ipAddresses?: string[];
|
type: string; // Ready
|
||||||
duration?: string;
|
}
|
||||||
renewBefore?: string;
|
|
||||||
isCA?: boolean;
|
|
||||||
keySize?: number;
|
|
||||||
keyAlgorithm?: "rsa" | "ecdsa";
|
|
||||||
issuerRef: {
|
|
||||||
kind?: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
acme?: {
|
|
||||||
config: {
|
|
||||||
domains: string[];
|
|
||||||
http01: {
|
|
||||||
ingress?: string;
|
|
||||||
ingressClass?: string;
|
|
||||||
};
|
|
||||||
dns01?: {
|
|
||||||
provider: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
status: {
|
|
||||||
conditions?: {
|
|
||||||
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
|
|
||||||
message: string; // Certificate is up to date and has not expired,
|
|
||||||
reason: string; // Ready,
|
|
||||||
status: string; // True,
|
|
||||||
type: string; // Ready
|
|
||||||
}[];
|
|
||||||
notAfter: string; // 2019-11-01T05:36:27Z
|
|
||||||
lastFailureTime?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
getType(): string {
|
export interface IssuerCondition extends IssuerConditionBase {
|
||||||
const { isCA, acme } = this.spec;
|
tooltip: string;
|
||||||
if (isCA) return "CA"
|
isReady: boolean;
|
||||||
if (acme) return "ACME"
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommonName() {
|
|
||||||
return this.spec.commonName || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
getIssuerName() {
|
|
||||||
return this.spec.issuerRef.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSecretName() {
|
|
||||||
return this.spec.secretName;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIssuerDetailsUrl() {
|
|
||||||
return getDetailsUrl(issuersApi.getUrl({
|
|
||||||
namespace: this.getNs(),
|
|
||||||
name: this.getIssuerName(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
getSecretDetailsUrl() {
|
|
||||||
return getDetailsUrl(secretsApi.getUrl({
|
|
||||||
namespace: this.getNs(),
|
|
||||||
name: this.getSecretName(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
getConditions() {
|
|
||||||
const { conditions = [] } = this.status;
|
|
||||||
return conditions.map(condition => {
|
|
||||||
const { message, reason, lastTransitionTime, status } = condition;
|
|
||||||
return {
|
|
||||||
...condition,
|
|
||||||
isReady: status === "True",
|
|
||||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Issuer extends KubeObject {
|
export class Issuer extends KubeObject {
|
||||||
@ -103,23 +30,23 @@ export class Issuer extends KubeObject {
|
|||||||
email: string;
|
email: string;
|
||||||
server: string;
|
server: string;
|
||||||
skipTLSVerify?: boolean;
|
skipTLSVerify?: boolean;
|
||||||
privateKeySecretRef: ISecretRef;
|
privateKeySecretRef: SecretRef;
|
||||||
solvers?: {
|
solvers?: {
|
||||||
dns01?: {
|
dns01?: {
|
||||||
cnameStrategy: string;
|
cnameStrategy: string;
|
||||||
acmedns?: {
|
acmedns?: {
|
||||||
host: string;
|
host: string;
|
||||||
accountSecretRef: ISecretRef;
|
accountSecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
akamai?: {
|
akamai?: {
|
||||||
accessTokenSecretRef: ISecretRef;
|
accessTokenSecretRef: SecretRef;
|
||||||
clientSecretSecretRef: ISecretRef;
|
clientSecretSecretRef: SecretRef;
|
||||||
clientTokenSecretRef: ISecretRef;
|
clientTokenSecretRef: SecretRef;
|
||||||
serviceConsumerDomain: string;
|
serviceConsumerDomain: string;
|
||||||
};
|
};
|
||||||
azuredns?: {
|
azuredns?: {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
clientSecretSecretRef: ISecretRef;
|
clientSecretSecretRef: SecretRef;
|
||||||
hostedZoneName: string;
|
hostedZoneName: string;
|
||||||
resourceGroupName: string;
|
resourceGroupName: string;
|
||||||
subscriptionID: string;
|
subscriptionID: string;
|
||||||
@ -127,26 +54,26 @@ export class Issuer extends KubeObject {
|
|||||||
};
|
};
|
||||||
clouddns?: {
|
clouddns?: {
|
||||||
project: string;
|
project: string;
|
||||||
serviceAccountSecretRef: ISecretRef;
|
serviceAccountSecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
cloudflare?: {
|
cloudflare?: {
|
||||||
email: string;
|
email: string;
|
||||||
apiKeySecretRef: ISecretRef;
|
apiKeySecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
digitalocean?: {
|
digitalocean?: {
|
||||||
tokenSecretRef: ISecretRef;
|
tokenSecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
rfc2136?: {
|
rfc2136?: {
|
||||||
nameserver: string;
|
nameserver: string;
|
||||||
tsigAlgorithm: string;
|
tsigAlgorithm: string;
|
||||||
tsigKeyName: string;
|
tsigKeyName: string;
|
||||||
tsigSecretSecretRef: ISecretRef;
|
tsigSecretSecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
route53?: {
|
route53?: {
|
||||||
accessKeyID: string;
|
accessKeyID: string;
|
||||||
hostedZoneID: string;
|
hostedZoneID: string;
|
||||||
region: string;
|
region: string;
|
||||||
secretAccessKeySecretRef: ISecretRef;
|
secretAccessKeySecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
webhook?: {
|
webhook?: {
|
||||||
config: object; // arbitrary json
|
config: object; // arbitrary json
|
||||||
@ -180,7 +107,7 @@ export class Issuer extends KubeObject {
|
|||||||
appRole: {
|
appRole: {
|
||||||
path: string;
|
path: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
secretRef: ISecretRef;
|
secretRef: SecretRef;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -188,7 +115,7 @@ export class Issuer extends KubeObject {
|
|||||||
venafi?: {
|
venafi?: {
|
||||||
zone: string;
|
zone: string;
|
||||||
cloud?: {
|
cloud?: {
|
||||||
apiTokenSecretRef: ISecretRef;
|
apiTokenSecretRef: SecretRef;
|
||||||
};
|
};
|
||||||
tpp?: {
|
tpp?: {
|
||||||
url: string;
|
url: string;
|
||||||
@ -204,25 +131,29 @@ export class Issuer extends KubeObject {
|
|||||||
acme?: {
|
acme?: {
|
||||||
uri: string;
|
uri: string;
|
||||||
};
|
};
|
||||||
conditions?: {
|
conditions?: IssuerConditionBase[];
|
||||||
lastTransitionTime: string; // 2019-06-05T07:10:42Z,
|
|
||||||
message: string; // The ACME account was registered with the ACME server,
|
|
||||||
reason: string; // ACMEAccountRegistered,
|
|
||||||
status: string; // True,
|
|
||||||
type: string; // Ready
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getType() {
|
getType(): IssuerType {
|
||||||
const { acme, ca, selfSigned, vault, venafi } = this.spec;
|
const { acme, ca, selfSigned, vault, venafi } = this.spec;
|
||||||
if (acme) return "ACME"
|
if (acme) {
|
||||||
if (ca) return "CA"
|
return "ACME";
|
||||||
if (selfSigned) return "SelfSigned"
|
}
|
||||||
if (vault) return "Vault"
|
if (ca) {
|
||||||
if (venafi) return "Venafi"
|
return "CA";
|
||||||
|
}
|
||||||
|
if (selfSigned) {
|
||||||
|
return "SelfSigned";
|
||||||
|
}
|
||||||
|
if (vault) {
|
||||||
|
return "Vault";
|
||||||
|
}
|
||||||
|
if (venafi) {
|
||||||
|
return "Venafi";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
getConditions(): IssuerCondition[] {
|
||||||
const { conditions = [] } = this.status;
|
const { conditions = [] } = this.status;
|
||||||
return conditions.map(condition => {
|
return conditions.map(condition => {
|
||||||
const { message, reason, lastTransitionTime, status } = condition;
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
@ -230,7 +161,113 @@ export class Issuer extends KubeObject {
|
|||||||
...condition,
|
...condition,
|
||||||
isReady: status === "True",
|
isReady: status === "True",
|
||||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issuersApi = new KubeApi({
|
||||||
|
kind: Issuer.kind,
|
||||||
|
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
|
||||||
|
isNamespaced: true,
|
||||||
|
objectConstructor: Issuer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CertificateConditionBase {
|
||||||
|
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
|
||||||
|
message: string; // Certificate is up to date and has not expired,
|
||||||
|
reason: string; // Ready,
|
||||||
|
status: string; // True,
|
||||||
|
type: string; // Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertificateCondition extends CertificateConditionBase {
|
||||||
|
isReady: boolean;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Certificate extends KubeObject {
|
||||||
|
static kind = "Certificate"
|
||||||
|
|
||||||
|
spec: {
|
||||||
|
secretName: string;
|
||||||
|
commonName?: string;
|
||||||
|
dnsNames?: string[];
|
||||||
|
organization?: string[];
|
||||||
|
ipAddresses?: string[];
|
||||||
|
duration?: string;
|
||||||
|
renewBefore?: string;
|
||||||
|
isCA?: boolean;
|
||||||
|
keySize?: number;
|
||||||
|
keyAlgorithm?: "rsa" | "ecdsa";
|
||||||
|
issuerRef: {
|
||||||
|
kind?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
acme?: {
|
||||||
|
config: {
|
||||||
|
domains: string[];
|
||||||
|
http01: {
|
||||||
|
ingress?: string;
|
||||||
|
ingressClass?: string;
|
||||||
|
};
|
||||||
|
dns01?: {
|
||||||
|
provider: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
status: {
|
||||||
|
conditions?: CertificateConditionBase[];
|
||||||
|
notAfter: string; // 2019-11-01T05:36:27Z
|
||||||
|
lastFailureTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(): string {
|
||||||
|
const { isCA, acme } = this.spec;
|
||||||
|
if (isCA) {
|
||||||
|
return "CA";
|
||||||
|
}
|
||||||
|
if (acme) {
|
||||||
|
return "ACME";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommonName(): string {
|
||||||
|
return this.spec.commonName || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssuerName(): string {
|
||||||
|
return this.spec.issuerRef.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSecretName(): string {
|
||||||
|
return this.spec.secretName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssuerDetailsUrl(): string {
|
||||||
|
return getDetailsUrl(issuersApi.getUrl({
|
||||||
|
namespace: this.getNs(),
|
||||||
|
name: this.getIssuerName(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSecretDetailsUrl(): string {
|
||||||
|
return getDetailsUrl(secretsApi.getUrl({
|
||||||
|
namespace: this.getNs(),
|
||||||
|
name: this.getSecretName(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getConditions(): CertificateCondition[] {
|
||||||
|
const { conditions = [] } = this.status;
|
||||||
|
return conditions.map(condition => {
|
||||||
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
|
return {
|
||||||
|
...condition,
|
||||||
|
isReady: status === "True",
|
||||||
|
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,13 +283,6 @@ export const certificatesApi = new KubeApi({
|
|||||||
objectConstructor: Certificate,
|
objectConstructor: Certificate,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const issuersApi = new KubeApi({
|
|
||||||
kind: Issuer.kind,
|
|
||||||
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
|
|
||||||
isNamespaced: true,
|
|
||||||
objectConstructor: Issuer,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clusterIssuersApi = new KubeApi({
|
export const clusterIssuersApi = new KubeApi({
|
||||||
kind: ClusterIssuer.kind,
|
kind: ClusterIssuer.kind,
|
||||||
apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers",
|
apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers",
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
|
import { Metrics, MetricsReqParams, metricsApi } from "./metrics.api";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class ClusterApi extends KubeApi<Cluster> {
|
export class ClusterApi extends KubeApi<Cluster> {
|
||||||
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
async getMetrics(nodeNames: string[], params?: MetricsReqParams): Promise<ClusterMetrics> {
|
||||||
const nodes = nodeNames.join("|");
|
const nodes = nodeNames.join("|");
|
||||||
const opts = { category: "cluster", nodes: nodes }
|
const opts = { category: "cluster", nodes: nodes };
|
||||||
|
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
memoryUsage: opts,
|
memoryUsage: opts,
|
||||||
@ -31,7 +31,7 @@ export enum ClusterStatus {
|
|||||||
ERROR = "Error"
|
ERROR = "Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IClusterMetrics<T = IMetrics> {
|
export interface ClusterMetrics<T = Metrics> {
|
||||||
[metric: string]: T;
|
[metric: string]: T;
|
||||||
memoryUsage: T;
|
memoryUsage: T;
|
||||||
memoryRequests: T;
|
memoryRequests: T;
|
||||||
@ -82,10 +82,16 @@ export class Cluster extends KubeObject {
|
|||||||
errorReason?: string;
|
errorReason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus(): ClusterStatus {
|
||||||
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
|
if (this.metadata.deletionTimestamp) {
|
||||||
if (!this.status || !this.status) return ClusterStatus.CREATING;
|
return ClusterStatus.REMOVING;
|
||||||
if (this.status.errorMessage) return ClusterStatus.ERROR;
|
}
|
||||||
|
if (!this.status || !this.status) {
|
||||||
|
return ClusterStatus.CREATING;
|
||||||
|
}
|
||||||
|
if (this.status.errorMessage) {
|
||||||
|
return ClusterStatus.ERROR;
|
||||||
|
}
|
||||||
return ClusterStatus.ACTIVE;
|
return ClusterStatus.ACTIVE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export interface IComponentStatusCondition {
|
export interface ComponentStatusCondition {
|
||||||
type: string;
|
type: string;
|
||||||
status: string;
|
status: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -10,9 +10,9 @@ export interface IComponentStatusCondition {
|
|||||||
export class ComponentStatus extends KubeObject {
|
export class ComponentStatus extends KubeObject {
|
||||||
static kind = "ComponentStatus"
|
static kind = "ComponentStatus"
|
||||||
|
|
||||||
conditions: IComponentStatusCondition[]
|
conditions: ComponentStatusCondition[]
|
||||||
|
|
||||||
getTruthyConditions() {
|
getTruthyConditions(): ComponentStatusCondition[] {
|
||||||
return this.conditions.filter(c => c.status === "True");
|
return this.conditions.filter(c => c.status === "True");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
// App configuration api
|
// App configuration api
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { IConfig } from "../../../server/common/config";
|
import { IConfig } from "../../../server/common/config";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
|
||||||
export const configApi = {
|
export const configApi = {
|
||||||
getConfig() {
|
getConfig(): CancelablePromise<IConfig> {
|
||||||
return apiBase.get<IConfig>("/config")
|
return apiBase.get<IConfig>("/config");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,81 +51,80 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
storedVersions: string[];
|
storedVersions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceUrl() {
|
getResourceUrl(): string {
|
||||||
return crdResourcesURL({
|
return crdResourcesURL({
|
||||||
params: {
|
params: {
|
||||||
group: this.getGroup(),
|
group: this.getGroup(),
|
||||||
name: this.getPluralName(),
|
name: this.getPluralName(),
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceApiBase() {
|
getResourceApiBase(): string {
|
||||||
const { version, group } = this.spec;
|
const { version, group } = this.spec;
|
||||||
return `/apis/${group}/${version}/${this.getPluralName()}`
|
return `/apis/${group}/${version}/${this.getPluralName()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPluralName() {
|
getPluralName(): string {
|
||||||
return this.getNames().plural
|
return this.getNames().plural;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceKind() {
|
getResourceKind(): string {
|
||||||
return this.spec.names.kind
|
return this.spec.names.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceTitle() {
|
getResourceTitle(): string {
|
||||||
const name = this.getPluralName();
|
const name = this.getPluralName();
|
||||||
return name[0].toUpperCase() + name.substr(1)
|
return name[0].toUpperCase() + name.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroup() {
|
getGroup(): string {
|
||||||
return this.spec.group;
|
return this.spec.group;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScope() {
|
getScope(): string {
|
||||||
return this.spec.scope;
|
return this.spec.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion(): string {
|
||||||
return this.spec.version;
|
return this.spec.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
isNamespaced() {
|
isNamespaced(): boolean {
|
||||||
return this.getScope() === "Namespaced";
|
return this.getScope() === "Namespaced";
|
||||||
}
|
}
|
||||||
|
|
||||||
getStoredVersions() {
|
getStoredVersions(): string {
|
||||||
return this.status.storedVersions.join(", ");
|
return this.status.storedVersions.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
getNames() {
|
getNames(): CustomResourceDefinition["spec"]["names"] {
|
||||||
return this.spec.names;
|
return this.spec.names;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversion() {
|
getConversion(): string {
|
||||||
return JSON.stringify(this.spec.conversion);
|
return JSON.stringify(this.spec.conversion);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrinterColumns(ignorePriority = true) {
|
getPrinterColumns(ignorePriority = true): Required<CustomResourceDefinition["spec"]["additionalPrinterColumns"]> {
|
||||||
const columns = this.spec.additionalPrinterColumns || [];
|
const columns = this.spec.additionalPrinterColumns || [];
|
||||||
return columns
|
return columns
|
||||||
.filter(column => column.name != "Age")
|
.filter(column => column.name != "Age")
|
||||||
.filter(column => ignorePriority ? true : !column.priority);
|
.filter(column => ignorePriority ? true : !column.priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidation() {
|
getValidation(): string {
|
||||||
return JSON.stringify(this.spec.validation, null, 2);
|
return JSON.stringify(this.spec.validation, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
getConditions(): Required<CustomResourceDefinition["status"]["conditions"]> {
|
||||||
if (!this.status.conditions) return [];
|
return (this.status.conditions || []).map(condition => {
|
||||||
return this.status.conditions.map(condition => {
|
|
||||||
const { message, reason, lastTransitionTime, status } = condition;
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
return {
|
return {
|
||||||
...condition,
|
...condition,
|
||||||
isReady: status === "True",
|
isReady: status === "True",
|
||||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { IPodContainer } from "./pods.api";
|
import { PodContainer } from "./pods.api";
|
||||||
import { formatDuration } from "../../utils/formatDuration";
|
import { formatDuration } from "../../utils/formatDuration";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
@ -39,7 +39,7 @@ export class CronJob extends KubeObject {
|
|||||||
creationTimestamp?: string;
|
creationTimestamp?: string;
|
||||||
};
|
};
|
||||||
spec: {
|
spec: {
|
||||||
containers: IPodContainer[];
|
containers: PodContainer[];
|
||||||
restartPolicy: string;
|
restartPolicy: string;
|
||||||
terminationGracePeriodSeconds: number;
|
terminationGracePeriodSeconds: number;
|
||||||
dnsPolicy: string;
|
dnsPolicy: string;
|
||||||
@ -56,26 +56,27 @@ export class CronJob extends KubeObject {
|
|||||||
lastScheduleTime: string;
|
lastScheduleTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuspendFlag() {
|
getSuspendFlag(): string {
|
||||||
return this.spec.suspend.toString()
|
return this.spec.suspend.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastScheduleTime() {
|
getLastScheduleTime(): string {
|
||||||
const diff = moment().diff(this.status.lastScheduleTime)
|
return formatDuration(moment().diff(this.status.lastScheduleTime), true);
|
||||||
return formatDuration(diff, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSchedule() {
|
getSchedule(): string {
|
||||||
return this.spec.schedule
|
return this.spec.schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
isNeverRun() {
|
isNeverRun(): boolean {
|
||||||
const schedule = this.getSchedule();
|
const schedule = this.getSchedule();
|
||||||
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
const stamps = schedule.split(" ");
|
const stamps = schedule.split(" ");
|
||||||
const day = Number(stamps[stamps.length - 3]); // 1-31
|
const day = Number(stamps[stamps.length - 3]); // 1-31
|
||||||
const month = Number(stamps[stamps.length - 2]); // 1-12
|
const month = Number(stamps[stamps.length - 2]); // 1-12
|
||||||
if (schedule.startsWith("@")) return false;
|
if (schedule.startsWith("@")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return day > daysInMonth[month - 1];
|
return day > daysInMonth[month - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { IPodContainer } from "./pods.api";
|
import { PodContainer } from "./pods.api";
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@ -22,13 +22,13 @@ export class DaemonSet extends WorkloadKubeObject {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
spec: {
|
spec: {
|
||||||
containers: IPodContainer[];
|
containers: PodContainer[];
|
||||||
initContainers?: IPodContainer[];
|
initContainers?: PodContainer[];
|
||||||
restartPolicy: string;
|
restartPolicy: string;
|
||||||
terminationGracePeriodSeconds: number;
|
terminationGracePeriodSeconds: number;
|
||||||
dnsPolicy: string;
|
dnsPolicy: string;
|
||||||
hostPID: boolean;
|
hostPID: boolean;
|
||||||
affinity?: IAffinity;
|
affinity?: Affinity;
|
||||||
nodeSelector?: {
|
nodeSelector?: {
|
||||||
[selector: string]: string;
|
[selector: string]: string;
|
||||||
};
|
};
|
||||||
@ -61,10 +61,10 @@ export class DaemonSet extends WorkloadKubeObject {
|
|||||||
numberUnavailable: number;
|
numberUnavailable: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages(): string[] {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
|
const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||||
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", [])
|
const initContainers: PodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
||||||
return [...containers, ...initContainers].map(container => container.image)
|
return [...containers, ...initContainers].map(container => container.image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,24 @@
|
|||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
import { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
export class DeploymentApi extends KubeApi<Deployment> {
|
export class DeploymentApi extends KubeApi<Deployment> {
|
||||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
protected getScaleApiUrl(params: { namespace: string; name: string }): string {
|
||||||
return this.getUrl(params) + "/scale"
|
return this.getUrl(params) + "/scale";
|
||||||
}
|
}
|
||||||
|
|
||||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||||
return this.request
|
const { status } = await this.request.get(this.getScaleApiUrl(params));
|
||||||
.get(this.getScaleApiUrl(params))
|
return status.replicas;
|
||||||
.then(({ status }: any) => status.replicas)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
scale(metadata: { namespace: string; name: string }, replicas: number): CancelablePromise<KubeJsonApiData> {
|
||||||
return this.request.put(this.getScaleApiUrl(params), {
|
return this.request.put(
|
||||||
data: {
|
this.getScaleApiUrl(metadata),
|
||||||
metadata: params,
|
{ data: { metadata, spec: { replicas } } }
|
||||||
spec: {
|
);
|
||||||
replicas: replicas
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +93,7 @@ export class Deployment extends WorkloadKubeObject {
|
|||||||
restartPolicy: string;
|
restartPolicy: string;
|
||||||
terminationGracePeriodSeconds: number;
|
terminationGracePeriodSeconds: number;
|
||||||
dnsPolicy: string;
|
dnsPolicy: string;
|
||||||
affinity?: IAffinity;
|
affinity?: Affinity;
|
||||||
nodeSelector?: {
|
nodeSelector?: {
|
||||||
[selector: string]: string;
|
[selector: string]: string;
|
||||||
};
|
};
|
||||||
@ -145,20 +142,15 @@ export class Deployment extends WorkloadKubeObject {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions(activeOnly = false) {
|
getConditions(activeOnly = false): Deployment["status"]["conditions"] {
|
||||||
const { conditions } = this.status
|
return this.status.conditions.filter(({ status }) => !activeOnly || status === "True");
|
||||||
if (!conditions) return []
|
|
||||||
if (activeOnly) {
|
|
||||||
return conditions.filter(c => c.status === "True")
|
|
||||||
}
|
|
||||||
return conditions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditionsText(activeOnly = true) {
|
getConditionsText(activeOnly = true): string {
|
||||||
return this.getConditions(activeOnly).map(({ type }) => type).join(" ")
|
return this.getConditions(activeOnly).map(({ type }) => type).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
getReplicas() {
|
getReplicas(): number {
|
||||||
return this.spec.replicas || 0;
|
return this.spec.replicas || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,25 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export interface IEndpointPort {
|
export interface EndpointPort {
|
||||||
name?: string;
|
name?: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndpointAddress {
|
export interface EndpointAddress {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndpointSubset {
|
export interface EndpointSubset {
|
||||||
addresses: IEndpointAddress[];
|
addresses: EndpointAddress[];
|
||||||
notReadyAddresses: IEndpointAddress[];
|
notReadyAddresses: EndpointAddress[];
|
||||||
ports: IEndpointPort[];
|
ports: EndpointPort[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITargetRef {
|
interface TargetRef {
|
||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -29,7 +29,7 @@ interface ITargetRef {
|
|||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointAddress implements IEndpointAddress {
|
export class EndpointAddress implements EndpointAddress {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
@ -41,34 +41,34 @@ export class EndpointAddress implements IEndpointAddress {
|
|||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(data: IEndpointAddress) {
|
constructor(data: EndpointAddress) {
|
||||||
Object.assign(this, data)
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId(): string {
|
||||||
return this.ip
|
return this.ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return this.hostname
|
return this.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetRef(): ITargetRef {
|
getTargetRef(): TargetRef {
|
||||||
if (this.targetRef) {
|
if (this.targetRef) {
|
||||||
return Object.assign(this.targetRef, {apiVersion: "v1"})
|
return Object.assign(this.targetRef, {apiVersion: "v1"});
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointSubset implements IEndpointSubset {
|
export class EndpointSubset implements EndpointSubset {
|
||||||
addresses: IEndpointAddress[];
|
addresses: EndpointAddress[];
|
||||||
notReadyAddresses: IEndpointAddress[];
|
notReadyAddresses: EndpointAddress[];
|
||||||
ports: IEndpointPort[];
|
ports: EndpointPort[];
|
||||||
|
|
||||||
constructor(data: IEndpointSubset) {
|
constructor(data: EndpointSubset) {
|
||||||
Object.assign(this, data)
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddresses(): EndpointAddress[] {
|
getAddresses(): EndpointAddress[] {
|
||||||
@ -83,16 +83,16 @@ export class EndpointSubset implements IEndpointSubset {
|
|||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
if(!this.addresses) {
|
if(!this.addresses) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
return this.addresses.map(address => {
|
return this.addresses.map(address => {
|
||||||
if (!this.ports) {
|
if (!this.ports) {
|
||||||
return address.ip
|
return address.ip;
|
||||||
}
|
}
|
||||||
return this.ports.map(port => {
|
return this.ports.map(port => {
|
||||||
return `${address.ip}:${port.port}`
|
return `${address.ip}:${port.port}`;
|
||||||
}).join(", ")
|
}).join(", ");
|
||||||
}).join(", ")
|
}).join(", ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export class EndpointSubset implements IEndpointSubset {
|
|||||||
export class Endpoint extends KubeObject {
|
export class Endpoint extends KubeObject {
|
||||||
static kind = "Endpoint"
|
static kind = "Endpoint"
|
||||||
|
|
||||||
subsets: IEndpointSubset[]
|
subsets: EndpointSubset[]
|
||||||
|
|
||||||
getEndpointSubsets(): EndpointSubset[] {
|
getEndpointSubsets(): EndpointSubset[] {
|
||||||
const subsets = this.subsets || [];
|
const subsets = this.subsets || [];
|
||||||
@ -109,9 +109,9 @@ export class Endpoint extends KubeObject {
|
|||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
if(this.subsets) {
|
if(this.subsets) {
|
||||||
return this.getEndpointSubsets().map(es => es.toString()).join(", ")
|
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
|
||||||
} else {
|
} else {
|
||||||
return "<none>"
|
return "<none>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,23 +31,23 @@ export class KubeEvent extends KubeObject {
|
|||||||
reportingComponent: string
|
reportingComponent: string
|
||||||
reportingInstance: string
|
reportingInstance: string
|
||||||
|
|
||||||
isWarning() {
|
isWarning(): boolean {
|
||||||
return this.type === "Warning";
|
return this.type === "Warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
getSource() {
|
getSource(): string {
|
||||||
const { component, host } = this.source
|
const { component, host } = this.source;
|
||||||
return `${component} ${host || ""}`
|
return `${component} ${host || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstSeenTime() {
|
getFirstSeenTime(): string {
|
||||||
const diff = moment().diff(this.firstTimestamp)
|
const diff = moment().diff(this.firstTimestamp);
|
||||||
return formatDuration(diff, true)
|
return formatDuration(diff, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastSeenTime() {
|
getLastSeenTime(): string {
|
||||||
const diff = moment().diff(this.lastTimestamp)
|
const diff = moment().diff(this.lastTimestamp);
|
||||||
return formatDuration(diff, true)
|
return formatDuration(diff, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,4 +56,4 @@ export const eventApi = new KubeApi({
|
|||||||
apiBase: "/api/v1/events",
|
apiBase: "/api/v1/events",
|
||||||
isNamespaced: true,
|
isNamespaced: true,
|
||||||
objectConstructor: KubeEvent,
|
objectConstructor: KubeEvent,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -2,54 +2,7 @@ import pathToRegExp from "path-to-regexp";
|
|||||||
import { apiKubeHelm } from "../index";
|
import { apiKubeHelm } from "../index";
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
interface IHelmChartList {
|
|
||||||
[repo: string]: {
|
|
||||||
[name: string]: HelmChart;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHelmChartDetails {
|
|
||||||
readme: string;
|
|
||||||
versions: HelmChart[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = pathToRegExp.compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
|
||||||
repo?: string;
|
|
||||||
name?: string;
|
|
||||||
}) => string;
|
|
||||||
|
|
||||||
export const helmChartsApi = {
|
|
||||||
list() {
|
|
||||||
return apiKubeHelm
|
|
||||||
.get<IHelmChartList>(endpoint())
|
|
||||||
.then(data => {
|
|
||||||
return Object
|
|
||||||
.values(data)
|
|
||||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
|
||||||
.map(HelmChart.create);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
get(repo: string, name: string, readmeVersion?: string) {
|
|
||||||
const path = endpoint({ repo, name });
|
|
||||||
return apiKubeHelm
|
|
||||||
.get<IHelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
|
|
||||||
.then(data => {
|
|
||||||
const versions = data.versions.map(HelmChart.create);
|
|
||||||
const readme = data.readme;
|
|
||||||
return {
|
|
||||||
readme,
|
|
||||||
versions,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getValues(repo: string, name: string, version: string) {
|
|
||||||
return apiKubeHelm
|
|
||||||
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class HelmChart {
|
export class HelmChart {
|
||||||
@ -57,10 +10,6 @@ export class HelmChart {
|
|||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(data: any) {
|
|
||||||
return new HelmChart(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
apiVersion: string
|
apiVersion: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
@ -83,47 +32,74 @@ export class HelmChart {
|
|||||||
deprecated?: boolean
|
deprecated?: boolean
|
||||||
tillerVersion?: string
|
tillerVersion?: string
|
||||||
|
|
||||||
getId() {
|
getId(): string {
|
||||||
return this.digest;
|
return this.digest;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullName(splitter = "/") {
|
getFullName(splitter = "/"): string {
|
||||||
return [this.getRepository(), this.getName()].join(splitter);
|
return [this.repo, this.name].join(splitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription() {
|
getMaintainers(): Required<HelmChart["maintainers"]> {
|
||||||
return this.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIcon() {
|
|
||||||
return this.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHome() {
|
|
||||||
return this.home;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaintainers() {
|
|
||||||
return this.maintainers || [];
|
return this.maintainers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getAppVersion(): string {
|
||||||
return this.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRepository() {
|
|
||||||
return this.repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAppVersion() {
|
|
||||||
return this.appVersion || "";
|
return this.appVersion || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeywords() {
|
getKeywords(): Required<HelmChart["keywords"]> {
|
||||||
return this.keywords || [];
|
return this.keywords || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HelmChartList {
|
||||||
|
[repo: string]: {
|
||||||
|
[name: string]: HelmChart;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HelmChartDetails {
|
||||||
|
readme: string;
|
||||||
|
versions: HelmChart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = pathToRegExp.compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
||||||
|
repo?: string;
|
||||||
|
name?: string;
|
||||||
|
}) => string;
|
||||||
|
|
||||||
|
export interface HelmChartInfo {
|
||||||
|
readme: string;
|
||||||
|
versions: HelmChart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const helmChartsApi = {
|
||||||
|
list(): CancelablePromise<HelmChart[]> {
|
||||||
|
return apiKubeHelm.get<HelmChartList>(endpoint())
|
||||||
|
.then(data => {
|
||||||
|
return Object.values(data)
|
||||||
|
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
||||||
|
.map(data => new HelmChart(data));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get(repo: string, name: string, readmeVersion?: string): CancelablePromise<HelmChartInfo> {
|
||||||
|
const path = endpoint({ repo, name });
|
||||||
|
|
||||||
|
return apiKubeHelm.get<HelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
|
||||||
|
.then(({ readme, versions }) => ({
|
||||||
|
readme,
|
||||||
|
versions: versions.map(data => new HelmChart(data))
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getValues(repo: string, name: string, version: string): CancelablePromise<string> {
|
||||||
|
return apiKubeHelm
|
||||||
|
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import { apiKubeHelm } from "../index";
|
|||||||
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
|
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
|
||||||
import { ItemObject } from "../../item.store";
|
import { ItemObject } from "../../item.store";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
import { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
interface IReleasePayload {
|
interface ReleasePayload {
|
||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
version: string;
|
version: string;
|
||||||
@ -23,15 +25,15 @@ interface IReleasePayload {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IReleaseRawDetails extends IReleasePayload {
|
interface ReleaseRawDetails extends ReleasePayload {
|
||||||
resources: string;
|
resources: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseDetails extends IReleasePayload {
|
export interface ReleaseInfo extends ReleasePayload {
|
||||||
resources: KubeObject[];
|
resources: KubeObject[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseCreatePayload {
|
export interface ReleaseCreatePayload {
|
||||||
name?: string;
|
name?: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
chart: string;
|
chart: string;
|
||||||
@ -40,19 +42,19 @@ export interface IReleaseCreatePayload {
|
|||||||
values: string;
|
values: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseUpdatePayload {
|
export interface ReleaseUpdatePayload {
|
||||||
repo: string;
|
repo: string;
|
||||||
chart: string;
|
chart: string;
|
||||||
version: string;
|
version: string;
|
||||||
values: string;
|
values: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseUpdateDetails {
|
export interface ReleaseUpdateDetails {
|
||||||
log: string;
|
log: string;
|
||||||
release: IReleaseDetails;
|
release: ReleaseInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseRevision {
|
export interface ReleaseRevision {
|
||||||
revision: number;
|
revision: number;
|
||||||
updated: string;
|
updated: string;
|
||||||
status: string;
|
status: string;
|
||||||
@ -67,74 +69,12 @@ const endpoint = pathToRegExp.compile(`/v2/releases/:namespace?/:name?`) as (
|
|||||||
}
|
}
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
export const helmReleasesApi = {
|
|
||||||
list(namespace?: string) {
|
|
||||||
return apiKubeHelm
|
|
||||||
.get<HelmRelease[]>(endpoint({ namespace }))
|
|
||||||
.then(releases => releases.map(HelmRelease.create));
|
|
||||||
},
|
|
||||||
|
|
||||||
get(name: string, namespace: string) {
|
|
||||||
const path = endpoint({ name, namespace });
|
|
||||||
return apiKubeHelm.get<IReleaseRawDetails>(path).then(details => {
|
|
||||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
|
||||||
const resources = items.map(item => KubeObject.create(item));
|
|
||||||
return {
|
|
||||||
...details,
|
|
||||||
resources
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
|
||||||
const { repo, ...data } = payload;
|
|
||||||
data.chart = `${repo}/${data.chart}`;
|
|
||||||
data.values = jsYaml.safeLoad(data.values);
|
|
||||||
return apiKubeHelm.post(endpoint(), { data });
|
|
||||||
},
|
|
||||||
|
|
||||||
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
|
||||||
const { repo, ...data } = payload;
|
|
||||||
data.chart = `${repo}/${data.chart}`;
|
|
||||||
data.values = jsYaml.safeLoad(data.values);
|
|
||||||
return apiKubeHelm.put(endpoint({ name, namespace }), { data });
|
|
||||||
},
|
|
||||||
|
|
||||||
async delete(name: string, namespace: string) {
|
|
||||||
const path = endpoint({ name, namespace });
|
|
||||||
return apiKubeHelm.del(path);
|
|
||||||
},
|
|
||||||
|
|
||||||
getValues(name: string, namespace: string) {
|
|
||||||
const path = endpoint({ name, namespace }) + "/values";
|
|
||||||
return apiKubeHelm.get<string>(path);
|
|
||||||
},
|
|
||||||
|
|
||||||
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
|
||||||
const path = endpoint({ name, namespace }) + "/history";
|
|
||||||
return apiKubeHelm.get(path);
|
|
||||||
},
|
|
||||||
|
|
||||||
rollback(name: string, namespace: string, revision: number) {
|
|
||||||
const path = endpoint({ name, namespace }) + "/rollback";
|
|
||||||
return apiKubeHelm.put(path, {
|
|
||||||
data: {
|
|
||||||
revision: revision
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class HelmRelease implements ItemObject {
|
export class HelmRelease implements ItemObject {
|
||||||
constructor(data: any) {
|
constructor(data: any) {
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(data: any) {
|
|
||||||
return new HelmRelease(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
appVersion: string
|
appVersion: string
|
||||||
name: string
|
name: string
|
||||||
namespace: string
|
namespace: string
|
||||||
@ -143,45 +83,32 @@ export class HelmRelease implements ItemObject {
|
|||||||
updated: string
|
updated: string
|
||||||
revision: number
|
revision: number
|
||||||
|
|
||||||
getId() {
|
getId(): string {
|
||||||
return this.namespace + this.name;
|
return this.namespace + this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNs() {
|
getChart(withVersion = false): string {
|
||||||
return this.namespace;
|
let chart = this.chart;
|
||||||
}
|
if (!withVersion && this.getVersion() != "") {
|
||||||
|
const search = new RegExp(`-${this.getVersion()}`);
|
||||||
getChart(withVersion = false) {
|
|
||||||
let chart = this.chart
|
|
||||||
if(!withVersion && this.getVersion() != "" ) {
|
|
||||||
const search = new RegExp(`-${this.getVersion()}`)
|
|
||||||
chart = chart.replace(search, "");
|
chart = chart.replace(search, "");
|
||||||
}
|
}
|
||||||
return chart
|
return chart;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRevision() {
|
getStatus(): string {
|
||||||
return this.revision;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatus() {
|
|
||||||
return capitalize(this.status);
|
return capitalize(this.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion(): string {
|
||||||
const versions = this.chart.match(/(v?\d+)[^-].*$/)
|
return this.chart.match(/(v?\d+)[^-].*$/)?.[0] || "";
|
||||||
if (versions) {
|
|
||||||
return versions[0]
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUpdated(humanize = true, compact = true) {
|
getUpdated(humanize = true, compact = true): number | string {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||||
const updatedDate = new Date(updated).getTime();
|
const updatedDate = new Date(updated).getTime();
|
||||||
@ -194,11 +121,67 @@ export class HelmRelease implements ItemObject {
|
|||||||
|
|
||||||
// Helm does not store from what repository the release is installed,
|
// Helm does not store from what repository the release is installed,
|
||||||
// so we have to try to guess it by searching charts
|
// so we have to try to guess it by searching charts
|
||||||
async getRepo() {
|
async getRepo(): Promise<string> {
|
||||||
const chartName = this.getChart();
|
const chartName = this.getChart();
|
||||||
const version = this.getVersion();
|
const version = this.getVersion();
|
||||||
const versions = await helmChartStore.getVersions(chartName);
|
const versions = await helmChartStore.getVersions(chartName);
|
||||||
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
|
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
|
||||||
return chartVersion ? chartVersion.repo : "";
|
return chartVersion?.repo || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const helmReleasesApi = {
|
||||||
|
async list(namespace?: string): Promise<HelmRelease[]> {
|
||||||
|
const releases = await apiKubeHelm.get<HelmRelease[]>(endpoint({ namespace }));
|
||||||
|
return releases.map(data => new HelmRelease(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(name: string, namespace: string): Promise<ReleaseInfo> {
|
||||||
|
const path = endpoint({ name, namespace });
|
||||||
|
const details = await apiKubeHelm.get<ReleaseRawDetails>(path);
|
||||||
|
const items: KubeObject[] = JSON.parse(details.resources).items;
|
||||||
|
const resources = items.map(item => new KubeObject(item));
|
||||||
|
return {
|
||||||
|
...details,
|
||||||
|
resources
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
create(payload: ReleaseCreatePayload): Promise<ReleaseUpdateDetails> {
|
||||||
|
const { repo, ...data } = payload;
|
||||||
|
data.chart = `${repo}/${data.chart}`;
|
||||||
|
data.values = jsYaml.safeLoad(data.values);
|
||||||
|
return apiKubeHelm.post(endpoint(), { data });
|
||||||
|
},
|
||||||
|
|
||||||
|
update(name: string, namespace: string, payload: ReleaseUpdatePayload): Promise<ReleaseUpdateDetails> {
|
||||||
|
const { repo, ...data } = payload;
|
||||||
|
data.chart = `${repo}/${data.chart}`;
|
||||||
|
data.values = jsYaml.safeLoad(data.values);
|
||||||
|
return apiKubeHelm.put(endpoint({ name, namespace }), { data });
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(name: string, namespace: string): CancelablePromise<KubeJsonApiData> {
|
||||||
|
const path = endpoint({ name, namespace });
|
||||||
|
return apiKubeHelm.del(path);
|
||||||
|
},
|
||||||
|
|
||||||
|
getValues(name: string, namespace: string): CancelablePromise<string> {
|
||||||
|
const path = endpoint({ name, namespace }) + "/values";
|
||||||
|
return apiKubeHelm.get<string>(path);
|
||||||
|
},
|
||||||
|
|
||||||
|
getHistory(name: string, namespace: string): Promise<ReleaseRevision[]> {
|
||||||
|
const path = endpoint({ name, namespace }) + "/history";
|
||||||
|
return apiKubeHelm.get(path);
|
||||||
|
},
|
||||||
|
|
||||||
|
rollback(name: string, namespace: string, revision: number): CancelablePromise<KubeJsonApiData> {
|
||||||
|
const path = endpoint({ name, namespace }) + "/rollback";
|
||||||
|
return apiKubeHelm.put(path, {
|
||||||
|
data: {
|
||||||
|
revision: revision
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export type IHpaMetricData<T = any> = T & {
|
|||||||
targetAverageValue?: string;
|
targetAverageValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHpaMetric {
|
export interface HpaMetric {
|
||||||
[kind: string]: IHpaMetricData;
|
[kind: string]: IHpaMetricData;
|
||||||
|
|
||||||
type: HpaMetricType;
|
type: HpaMetricType;
|
||||||
@ -38,6 +38,19 @@ export interface IHpaMetric {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PodStatusCondition {
|
||||||
|
lastTransitionTime: string;
|
||||||
|
message: string;
|
||||||
|
reason: string;
|
||||||
|
status: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PodCondition extends PodStatusCondition {
|
||||||
|
isReady: boolean;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class HorizontalPodAutoscaler extends KubeObject {
|
export class HorizontalPodAutoscaler extends KubeObject {
|
||||||
static kind = "HorizontalPodAutoscaler";
|
static kind = "HorizontalPodAutoscaler";
|
||||||
|
|
||||||
@ -49,58 +62,34 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
|||||||
};
|
};
|
||||||
minReplicas: number;
|
minReplicas: number;
|
||||||
maxReplicas: number;
|
maxReplicas: number;
|
||||||
metrics: IHpaMetric[];
|
metrics: HpaMetric[];
|
||||||
}
|
}
|
||||||
status: {
|
status: {
|
||||||
currentReplicas: number;
|
currentReplicas: number;
|
||||||
desiredReplicas: number;
|
desiredReplicas: number;
|
||||||
currentMetrics: IHpaMetric[];
|
currentMetrics: HpaMetric[];
|
||||||
conditions: {
|
conditions: PodStatusCondition[];
|
||||||
lastTransitionTime: string;
|
|
||||||
message: string;
|
|
||||||
reason: string;
|
|
||||||
status: string;
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxPods() {
|
getConditions(): PodCondition[] {
|
||||||
return this.spec.maxReplicas || 0;
|
if (!this.status.conditions) {
|
||||||
}
|
return [];
|
||||||
|
}
|
||||||
getMinPods() {
|
|
||||||
return this.spec.minReplicas || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getReplicas() {
|
|
||||||
return this.status.currentReplicas;
|
|
||||||
}
|
|
||||||
|
|
||||||
getConditions() {
|
|
||||||
if (!this.status.conditions) return [];
|
|
||||||
return this.status.conditions.map(condition => {
|
return this.status.conditions.map(condition => {
|
||||||
const { message, reason, lastTransitionTime, status } = condition;
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
return {
|
return {
|
||||||
...condition,
|
...condition,
|
||||||
isReady: status === "True",
|
isReady: status === "True",
|
||||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetrics() {
|
protected getMetricName(metric: HpaMetric): string {
|
||||||
return this.spec.metrics || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentMetrics() {
|
|
||||||
return this.status.currentMetrics || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getMetricName(metric: IHpaMetric): string {
|
|
||||||
const { type, resource, pods, object, external } = metric;
|
const { type, resource, pods, object, external } = metric;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case HpaMetricType.Resource:
|
case HpaMetricType.Resource:
|
||||||
return resource.name
|
return resource.name;
|
||||||
case HpaMetricType.Pods:
|
case HpaMetricType.Pods:
|
||||||
return pods.metricName;
|
return pods.metricName;
|
||||||
case HpaMetricType.Object:
|
case HpaMetricType.Object:
|
||||||
@ -111,9 +100,9 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo: refactor
|
// todo: refactor
|
||||||
getMetricValues(metric: IHpaMetric): string {
|
getMetricValues(metric: HpaMetric): string {
|
||||||
const metricType = metric.type.toLowerCase();
|
const metricType = metric.type.toLowerCase();
|
||||||
const currentMetric = this.getCurrentMetrics().find(current =>
|
const currentMetric = this.status.currentMetrics.find(current =>
|
||||||
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current)
|
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current)
|
||||||
);
|
);
|
||||||
const current = currentMetric ? currentMetric[metricType] : null;
|
const current = currentMetric ? currentMetric[metricType] : null;
|
||||||
@ -122,11 +111,15 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
|||||||
let targetValue = "unknown";
|
let targetValue = "unknown";
|
||||||
if (current) {
|
if (current) {
|
||||||
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
|
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
|
||||||
if (current.currentAverageUtilization) currentValue += "%";
|
if (current.currentAverageUtilization) {
|
||||||
|
currentValue += "%";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (target) {
|
if (target) {
|
||||||
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
|
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
|
||||||
if (target.targetAverageUtilization) targetValue += "%"
|
if (target.targetAverageUtilization) {
|
||||||
|
targetValue += "%";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return `${currentValue} / ${targetValue}`;
|
return `${currentValue} / ${targetValue}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,33 @@
|
|||||||
// Local express.js endpoints
|
// Local express.js endpoints
|
||||||
export * from "./config.api"
|
export * from "./config.api";
|
||||||
export * from "./cluster.api"
|
export * from "./cluster.api";
|
||||||
export * from "./kubeconfig.api"
|
export * from "./kubeconfig.api";
|
||||||
|
|
||||||
// Kubernetes endpoints
|
// Kubernetes endpoints
|
||||||
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
|
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
|
||||||
export * from "./namespaces.api"
|
export * from "./namespaces.api";
|
||||||
export * from "./cluster-role.api"
|
export * from "./cluster-role.api";
|
||||||
export * from "./cluster-role-binding.api"
|
export * from "./cluster-role-binding.api";
|
||||||
export * from "./role.api"
|
export * from "./role.api";
|
||||||
export * from "./role-binding.api"
|
export * from "./role-binding.api";
|
||||||
export * from "./secret.api"
|
export * from "./secret.api";
|
||||||
export * from "./service-accounts.api"
|
export * from "./service-accounts.api";
|
||||||
export * from "./nodes.api"
|
export * from "./nodes.api";
|
||||||
export * from "./pods.api"
|
export * from "./pods.api";
|
||||||
export * from "./deployment.api"
|
export * from "./deployment.api";
|
||||||
export * from "./daemon-set.api"
|
export * from "./daemon-set.api";
|
||||||
export * from "./stateful-set.api"
|
export * from "./stateful-set.api";
|
||||||
export * from "./replica-set.api"
|
export * from "./replica-set.api";
|
||||||
export * from "./job.api"
|
export * from "./job.api";
|
||||||
export * from "./cron-job.api"
|
export * from "./cron-job.api";
|
||||||
export * from "./configmap.api"
|
export * from "./configmap.api";
|
||||||
export * from "./ingress.api"
|
export * from "./ingress.api";
|
||||||
export * from "./network-policy.api"
|
export * from "./network-policy.api";
|
||||||
export * from "./persistent-volume-claims.api"
|
export * from "./persistent-volume-claims.api";
|
||||||
export * from "./persistent-volume.api"
|
export * from "./persistent-volume.api";
|
||||||
export * from "./service.api"
|
export * from "./service.api";
|
||||||
export * from "./endpoint.api"
|
export * from "./endpoint.api";
|
||||||
export * from "./storage-class.api"
|
export * from "./storage-class.api";
|
||||||
export * from "./pod-metrics.api"
|
export * from "./pod-metrics.api";
|
||||||
export * from "./podsecuritypolicy.api"
|
export * from "./podsecuritypolicy.api";
|
||||||
export * from "./selfsubjectrulesreviews.api"
|
export * from "./selfsubjectrulesreviews.api";
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { Metrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class IngressApi extends KubeApi<Ingress> {
|
export class IngressApi extends KubeApi<Ingress> {
|
||||||
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
getMetrics(ingress: string, namespace: string): Promise<IngressMetrics> {
|
||||||
const opts = { category: "ingress", ingress }
|
const opts = { category: "ingress", ingress };
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
bytesSentSuccess: opts,
|
bytesSentSuccess: opts,
|
||||||
bytesSentFailure: opts,
|
bytesSentFailure: opts,
|
||||||
@ -17,7 +17,7 @@ export class IngressApi extends KubeApi<Ingress> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIngressMetrics<T = IMetrics> {
|
export interface IngressMetrics<T = Metrics> {
|
||||||
[metric: string]: T;
|
[metric: string]: T;
|
||||||
bytesSentSuccess: T;
|
bytesSentSuccess: T;
|
||||||
bytesSentFailure: T;
|
bytesSentFailure: T;
|
||||||
@ -56,52 +56,55 @@ export class Ingress extends KubeObject {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutes() {
|
getRoutes(): string[] {
|
||||||
const { spec: { tls, rules } } = this
|
const { spec: { tls, rules } } = this;
|
||||||
if (!rules) return []
|
if (!rules) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let protocol = "http"
|
let protocol = "http";
|
||||||
const routes: string[] = []
|
const routes: string[] = [];
|
||||||
if (tls && tls.length > 0) {
|
if (tls && tls.length > 0) {
|
||||||
protocol += "s"
|
protocol += "s";
|
||||||
}
|
}
|
||||||
rules.map(rule => {
|
rules.map(rule => {
|
||||||
const host = rule.host ? rule.host : "*"
|
const host = rule.host ? rule.host : "*";
|
||||||
if (rule.http && rule.http.paths) {
|
if (rule.http && rule.http.paths) {
|
||||||
rule.http.paths.forEach(path => {
|
rule.http.paths.forEach(path => {
|
||||||
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort)
|
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHosts() {
|
getHosts(): string[] {
|
||||||
const { spec: { rules } } = this
|
const { spec: { rules } } = this;
|
||||||
if (!rules) return []
|
if (!rules) {
|
||||||
return rules.filter(rule => rule.host).map(rule => rule.host)
|
return [];
|
||||||
|
}
|
||||||
|
return rules.filter(rule => rule.host).map(rule => rule.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPorts() {
|
getPorts(): string {
|
||||||
const ports: number[] = []
|
const ports: number[] = [];
|
||||||
const { spec: { tls, rules, backend } } = this
|
const { spec: { tls, rules, backend } } = this;
|
||||||
const httpPort = 80
|
const httpPort = 80;
|
||||||
const tlsPort = 443
|
const tlsPort = 443;
|
||||||
if (rules && rules.length > 0) {
|
if (rules && rules.length > 0) {
|
||||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||||
ports.push(httpPort)
|
ports.push(httpPort);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (backend && backend.servicePort) {
|
if (backend && backend.servicePort) {
|
||||||
ports.push(backend.servicePort)
|
ports.push(backend.servicePort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tls && tls.length > 0) {
|
if (tls && tls.length > 0) {
|
||||||
ports.push(tlsPort)
|
ports.push(tlsPort);
|
||||||
}
|
}
|
||||||
return ports.join(", ")
|
return ports.join(", ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { IPodContainer } from "./pods.api";
|
import { PodContainer } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import { JsonApiParams } from "../json-api";
|
import { JsonApiParams } from "../json-api";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
import { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
|
export interface JobCondition {
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
lastProbeTime: string;
|
||||||
|
lastTransitionTime: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Job extends WorkloadKubeObject {
|
export class Job extends WorkloadKubeObject {
|
||||||
@ -26,12 +36,12 @@ export class Job extends WorkloadKubeObject {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
spec: {
|
spec: {
|
||||||
containers: IPodContainer[];
|
containers: PodContainer[];
|
||||||
restartPolicy: string;
|
restartPolicy: string;
|
||||||
terminationGracePeriodSeconds: number;
|
terminationGracePeriodSeconds: number;
|
||||||
dnsPolicy: string;
|
dnsPolicy: string;
|
||||||
hostPID: boolean;
|
hostPID: boolean;
|
||||||
affinity?: IAffinity;
|
affinity?: Affinity;
|
||||||
nodeSelector?: {
|
nodeSelector?: {
|
||||||
[selector: string]: string;
|
[selector: string]: string;
|
||||||
};
|
};
|
||||||
@ -44,7 +54,7 @@ export class Job extends WorkloadKubeObject {
|
|||||||
schedulerName: string;
|
schedulerName: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
containers?: IPodContainer[];
|
containers?: PodContainer[];
|
||||||
restartPolicy?: string;
|
restartPolicy?: string;
|
||||||
terminationGracePeriodSeconds?: number;
|
terminationGracePeriodSeconds?: number;
|
||||||
dnsPolicy?: string;
|
dnsPolicy?: string;
|
||||||
@ -53,48 +63,36 @@ export class Job extends WorkloadKubeObject {
|
|||||||
schedulerName?: string;
|
schedulerName?: string;
|
||||||
}
|
}
|
||||||
status: {
|
status: {
|
||||||
conditions: {
|
conditions: JobCondition[];
|
||||||
type: string;
|
|
||||||
status: string;
|
|
||||||
lastProbeTime: string;
|
|
||||||
lastTransitionTime: string;
|
|
||||||
message?: string;
|
|
||||||
}[];
|
|
||||||
startTime: string;
|
startTime: string;
|
||||||
completionTime: string;
|
completionTime: string;
|
||||||
succeeded: number;
|
succeeded: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDesiredCompletions() {
|
getDesiredCompletions(): number {
|
||||||
return this.spec.completions || 0;
|
return this.spec.completions || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions() {
|
getCompletions(): number {
|
||||||
return this.status.succeeded || 0;
|
return this.status.succeeded || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getParallelism() {
|
getCondition(): JobCondition {
|
||||||
return this.spec.parallelism;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCondition() {
|
|
||||||
// Type of Job condition could be only Complete or Failed
|
// Type of Job condition could be only Complete or Failed
|
||||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
||||||
const { conditions } = this.status;
|
return this.status.conditions.find(({ status }) => status === "True");
|
||||||
if (!conditions) return;
|
|
||||||
return conditions.find(({ status }) => status === "True");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages(): string[] {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
|
const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||||
return [...containers].map(container => container.image)
|
return containers.map(container => container.image);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
delete(): CancelablePromise<KubeJsonApiData> {
|
||||||
const params: JsonApiParams = {
|
const params: JsonApiParams = {
|
||||||
query: { propagationPolicy: "Background" }
|
query: { propagationPolicy: "Background" }
|
||||||
}
|
};
|
||||||
return super.delete(params)
|
return super.delete(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
// Kubeconfig api
|
// Kubeconfig api
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
import { JsonApiData } from "../json-api";
|
||||||
|
|
||||||
export const kubeConfigApi = {
|
export const kubeConfigApi = {
|
||||||
getUserConfig() {
|
getUserConfig(): CancelablePromise<JsonApiData> {
|
||||||
return apiBase.get("/kubeconfig/user");
|
return apiBase.get("/kubeconfig/user");
|
||||||
},
|
},
|
||||||
|
|
||||||
getServiceAccountConfig(account: string, namespace: string) {
|
getServiceAccountConfig(account: string, namespace: string): CancelablePromise<JsonApiData> {
|
||||||
return apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`);
|
return apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,15 +4,15 @@ import moment from "moment";
|
|||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { IMetricsQuery } from "../../../server/common/metrics";
|
import { IMetricsQuery } from "../../../server/common/metrics";
|
||||||
|
|
||||||
export interface IMetrics {
|
export interface Metrics {
|
||||||
status: string;
|
status: string;
|
||||||
data: {
|
data: {
|
||||||
resultType: string;
|
resultType: string;
|
||||||
result: IMetricsResult[];
|
result: MetricsResult[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetricsResult {
|
export interface MetricsResult {
|
||||||
metric: {
|
metric: {
|
||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
@ -25,7 +25,7 @@ export interface IMetricsResult {
|
|||||||
values: [number, string][];
|
values: [number, string][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetricsReqParams {
|
export interface MetricsReqParams {
|
||||||
start?: number | string; // timestamp in seconds or valid date-string
|
start?: number | string; // timestamp in seconds or valid date-string
|
||||||
end?: number | string;
|
end?: number | string;
|
||||||
step?: number; // step in seconds (default: 60s = each point 1m)
|
step?: number; // step in seconds (default: 60s = each point 1m)
|
||||||
@ -34,7 +34,7 @@ export interface IMetricsReqParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const metricsApi = {
|
export const metricsApi = {
|
||||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
|
async getMetrics<T = IMetricsQuery>(query: T, reqParams: MetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: Metrics } : Metrics> {
|
||||||
const { range = 3600, step = 60, namespace } = reqParams;
|
const { range = 3600, step = 60, namespace } = reqParams;
|
||||||
let { start, end } = reqParams;
|
let { start, end } = reqParams;
|
||||||
|
|
||||||
@ -55,18 +55,20 @@ export const metricsApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
export function normalizeMetrics(metrics: Metrics, frames = 60): Metrics {
|
||||||
if (!metrics?.data?.result) {
|
if (!metrics?.data?.result) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
resultType: "",
|
resultType: "",
|
||||||
result: [{
|
result: [{
|
||||||
metric: {},
|
metric: {
|
||||||
|
instance: "",
|
||||||
|
},
|
||||||
values: []
|
values: []
|
||||||
} as IMetricsResult],
|
}],
|
||||||
},
|
},
|
||||||
status: "",
|
status: "",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result } = metrics.data;
|
const { result } = metrics.data;
|
||||||
@ -75,34 +77,40 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
|||||||
if (frames > 0) {
|
if (frames > 0) {
|
||||||
// fill the gaps
|
// fill the gaps
|
||||||
result.forEach(res => {
|
result.forEach(res => {
|
||||||
if (!res.values || !res.values.length) return;
|
if (!res.values || !res.values.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
while (res.values.length < frames) {
|
while (res.values.length < frames) {
|
||||||
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
|
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
|
||||||
res.values.unshift([timestamp, "0"])
|
res.values.unshift([timestamp, "0"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// always return at least empty values array
|
// always return at least empty values array
|
||||||
result.push({
|
result.push({
|
||||||
metric: {},
|
metric: {
|
||||||
|
instance: "",
|
||||||
|
},
|
||||||
values: []
|
values: []
|
||||||
} as IMetricsResult);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
|
export function isMetricsEmpty(metrics: { [key: string]: Metrics }): boolean {
|
||||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
|
export function getItemMetrics(metrics: { [key: string]: Metrics }, itemName: string): { [key: string]: Metrics } {
|
||||||
if (!metrics) return;
|
if (!metrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const itemMetrics = { ...metrics };
|
const itemMetrics = { ...metrics };
|
||||||
for (const metric in metrics) {
|
for (const metric in metrics) {
|
||||||
if (!metrics[metric]?.data?.result) {
|
if (!metrics[metric]?.data?.result) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
const results = metrics[metric].data.result;
|
const results = metrics[metric].data.result;
|
||||||
const result = results.find(res => Object.values(res.metric)[0] == itemName);
|
const result = results.find(res => Object.values(res.metric)[0] == itemName);
|
||||||
@ -111,7 +119,7 @@ export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: s
|
|||||||
return itemMetrics;
|
return itemMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) {
|
export function getMetricLastPoints(metrics: { [key: string]: Metrics }): { [metric: string]: number } {
|
||||||
const result: Partial<{[metric: string]: number}> = {};
|
const result: Partial<{[metric: string]: number}> = {};
|
||||||
|
|
||||||
Object.keys(metrics).forEach(metricName => {
|
Object.keys(metrics).forEach(metricName => {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class Namespace extends KubeObject {
|
|||||||
phase: string;
|
phase: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus(): string {
|
||||||
return this.status ? this.status.phase : "-";
|
return this.status ? this.status.phase : "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,22 @@ import { KubeObject } from "../kube-object";
|
|||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export interface IPolicyIpBlock {
|
export interface PolicyIpBlock {
|
||||||
cidr: string;
|
cidr: string;
|
||||||
except?: string[];
|
except?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPolicySelector {
|
export interface PolicySelector {
|
||||||
matchLabels: {
|
matchLabels: {
|
||||||
[label: string]: string;
|
[label: string]: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPolicyIngress {
|
export interface PolicyIngress {
|
||||||
from: {
|
from: {
|
||||||
ipBlock?: IPolicyIpBlock;
|
ipBlock?: PolicyIpBlock;
|
||||||
namespaceSelector?: IPolicySelector;
|
namespaceSelector?: PolicySelector;
|
||||||
podSelector?: IPolicySelector;
|
podSelector?: PolicySelector;
|
||||||
}[];
|
}[];
|
||||||
ports: {
|
ports: {
|
||||||
protocol: string;
|
protocol: string;
|
||||||
@ -25,9 +25,9 @@ export interface IPolicyIngress {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPolicyEgress {
|
export interface PolicyEgress {
|
||||||
to: {
|
to: {
|
||||||
ipBlock: IPolicyIpBlock;
|
ipBlock: PolicyIpBlock;
|
||||||
}[];
|
}[];
|
||||||
ports: {
|
ports: {
|
||||||
protocol: string;
|
protocol: string;
|
||||||
@ -47,19 +47,23 @@ export class NetworkPolicy extends KubeObject {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
policyTypes: string[];
|
policyTypes: string[];
|
||||||
ingress: IPolicyIngress[];
|
ingress: PolicyIngress[];
|
||||||
egress: IPolicyEgress[];
|
egress: PolicyEgress[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchLabels(): string[] {
|
getMatchLabels(): string[] {
|
||||||
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
|
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return Object
|
return Object
|
||||||
.entries(this.spec.podSelector.matchLabels)
|
.entries(this.spec.podSelector.matchLabels)
|
||||||
.map(data => data.join(":"))
|
.map(data => data.join(":"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypes(): string[] {
|
getTypes(): string[] {
|
||||||
if (!this.spec.policyTypes) return [];
|
if (!this.spec.policyTypes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return this.spec.policyTypes;
|
return this.spec.policyTypes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
|
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { Metrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class NodesApi extends KubeApi<Node> {
|
export class NodesApi extends KubeApi<Node> {
|
||||||
getMetrics(): Promise<INodeMetrics> {
|
getMetrics(): Promise<NodeMetrics> {
|
||||||
const opts = { category: "nodes"}
|
const opts = { category: "nodes"};
|
||||||
|
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
memoryUsage: opts,
|
memoryUsage: opts,
|
||||||
@ -18,7 +18,7 @@ export class NodesApi extends KubeApi<Node> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeMetrics<T = IMetrics> {
|
export interface NodeMetrics<T = Metrics> {
|
||||||
[metric: string]: T;
|
[metric: string]: T;
|
||||||
memoryUsage: T;
|
memoryUsage: T;
|
||||||
memoryCapacity: T;
|
memoryCapacity: T;
|
||||||
@ -28,6 +28,21 @@ export interface INodeMetrics<T = IMetrics> {
|
|||||||
fsSize: T;
|
fsSize: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NodeTaint {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
effect: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeConditions {
|
||||||
|
type: string;
|
||||||
|
status?: string;
|
||||||
|
lastHeartbeatTime?: string;
|
||||||
|
lastTransitionTime?: string;
|
||||||
|
reason?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Node extends KubeObject {
|
export class Node extends KubeObject {
|
||||||
static kind = "Node"
|
static kind = "Node"
|
||||||
@ -35,11 +50,7 @@ export class Node extends KubeObject {
|
|||||||
spec: {
|
spec: {
|
||||||
podCIDR: string;
|
podCIDR: string;
|
||||||
externalID: string;
|
externalID: string;
|
||||||
taints?: {
|
taints?: NodeTaint[];
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
effect: string;
|
|
||||||
}[];
|
|
||||||
unschedulable?: boolean;
|
unschedulable?: boolean;
|
||||||
}
|
}
|
||||||
status: {
|
status: {
|
||||||
@ -53,14 +64,7 @@ export class Node extends KubeObject {
|
|||||||
memory: string;
|
memory: string;
|
||||||
pods: string;
|
pods: string;
|
||||||
};
|
};
|
||||||
conditions: {
|
conditions: NodeConditions[];
|
||||||
type: string;
|
|
||||||
status?: string;
|
|
||||||
lastHeartbeatTime?: string;
|
|
||||||
lastTransitionTime?: string;
|
|
||||||
reason?: string;
|
|
||||||
message?: string;
|
|
||||||
}[];
|
|
||||||
addresses: {
|
addresses: {
|
||||||
type: string;
|
type: string;
|
||||||
address: string;
|
address: string;
|
||||||
@ -83,75 +87,75 @@ export class Node extends KubeObject {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeConditionText() {
|
getNodeConditionText(): string {
|
||||||
const { conditions } = this.status
|
return this.status
|
||||||
if (!conditions) return ""
|
.conditions
|
||||||
return conditions.reduce((types, current) => {
|
.filter(({status}) => status === "True")
|
||||||
if (current.status !== "True") return ""
|
.map(({type}) => type)
|
||||||
return types += ` ${current.type}`
|
.join(" ");
|
||||||
}, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTaints() {
|
getTaints(): NodeTaint[] {
|
||||||
return this.spec.taints || [];
|
return this.spec.taints || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleLabels() {
|
getRoleLabels(): string {
|
||||||
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
|
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
|
||||||
key.includes("node-role.kubernetes.io")
|
key.includes("node-role.kubernetes.io")
|
||||||
).map(key => key.match(/([^/]+$)/)[0]) // all after last slash
|
).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
||||||
|
|
||||||
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
||||||
roleLabels.push(this.metadata.labels["kubernetes.io/role"])
|
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return roleLabels.join(", ")
|
return roleLabels.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
getCpuCapacity() {
|
getCpuCapacity(): number {
|
||||||
if (!this.status.capacity || !this.status.capacity.cpu) return 0
|
if (!this.status.capacity || !this.status.capacity.cpu) {
|
||||||
return cpuUnitsToNumber(this.status.capacity.cpu)
|
return 0;
|
||||||
|
}
|
||||||
|
return cpuUnitsToNumber(this.status.capacity.cpu);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMemoryCapacity() {
|
getMemoryCapacity(): number {
|
||||||
if (!this.status.capacity || !this.status.capacity.memory) return 0
|
if (!this.status.capacity || !this.status.capacity.memory) {
|
||||||
return unitsToBytes(this.status.capacity.memory)
|
return 0;
|
||||||
|
}
|
||||||
|
return unitsToBytes(this.status.capacity.memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
getConditions(): NodeConditions[] {
|
||||||
const conditions = this.status.conditions || [];
|
|
||||||
if (this.isUnschedulable()) {
|
if (this.isUnschedulable()) {
|
||||||
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
|
return [{ type: "SchedulingDisabled", status: "True" }, ...this.status.conditions];
|
||||||
}
|
}
|
||||||
return conditions;
|
return this.status.conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveConditions() {
|
getActiveConditions(): NodeConditions[] {
|
||||||
return this.getConditions().filter(c => c.status === "True");
|
return this.getConditions().filter(c => c.status === "True");
|
||||||
}
|
}
|
||||||
|
|
||||||
getWarningConditions() {
|
getWarningConditions(): NodeConditions[] {
|
||||||
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
|
const goodConditions = new Set(["Ready", "HostUpgrades", "SchedulingDisabled"]);
|
||||||
return this.getActiveConditions().filter(condition => {
|
return this.getActiveConditions().filter(condition => !goodConditions.has(condition.type));
|
||||||
return !goodConditions.includes(condition.type);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getKubeletVersion() {
|
getKubeletVersion(): string {
|
||||||
return this.status.nodeInfo.kubeletVersion;
|
return this.status.nodeInfo.kubeletVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOperatingSystem(): string {
|
getOperatingSystem(): string {
|
||||||
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="))
|
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
|
||||||
if (label) {
|
if (label) {
|
||||||
return label.split("=", 2)[1]
|
return label.split("=", 2)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
return "linux"
|
return "linux";
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnschedulable() {
|
isUnschedulable(): boolean {
|
||||||
return this.spec.unschedulable
|
return this.spec.unschedulable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { MatchExpression } from "../workload-kube-object";
|
||||||
|
import { Metrics, metricsApi } from "./metrics.api";
|
||||||
import { Pod } from "./pods.api";
|
import { Pod } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
||||||
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> {
|
getMetrics(pvcName: string, namespace: string): Promise<PvcMetrics> {
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
diskUsage: { category: 'pvc', pvc: pvcName },
|
diskUsage: { category: 'pvc', pvc: pvcName },
|
||||||
diskCapacity: { category: 'pvc', pvc: pvcName }
|
diskCapacity: { category: 'pvc', pvc: pvcName }
|
||||||
@ -15,7 +16,7 @@ export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPvcMetrics<T = IMetrics> {
|
export interface PvcMetrics<T = Metrics> {
|
||||||
[key: string]: T;
|
[key: string]: T;
|
||||||
diskUsage: T;
|
diskUsage: T;
|
||||||
diskCapacity: T;
|
diskCapacity: T;
|
||||||
@ -32,11 +33,7 @@ export class PersistentVolumeClaim extends KubeObject {
|
|||||||
matchLabels: {
|
matchLabels: {
|
||||||
release: string;
|
release: string;
|
||||||
};
|
};
|
||||||
matchExpressions: {
|
matchExpressions: MatchExpression[];
|
||||||
key: string; // environment,
|
|
||||||
operator: string; // In,
|
|
||||||
values: string[]; // [dev]
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
resources: {
|
resources: {
|
||||||
requests: {
|
requests: {
|
||||||
@ -49,34 +46,39 @@ export class PersistentVolumeClaim extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPods(allPods: Pod[]): Pod[] {
|
getPods(allPods: Pod[]): Pod[] {
|
||||||
const pods = allPods.filter(pod => pod.getNs() === this.getNs())
|
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
|
||||||
return pods.filter(pod => {
|
return pods.filter(pod => {
|
||||||
return pod.getVolumes().filter(volume =>
|
return pod.getVolumes().filter(volume =>
|
||||||
volume.persistentVolumeClaim &&
|
volume.persistentVolumeClaim &&
|
||||||
volume.persistentVolumeClaim.claimName === this.getName()
|
volume.persistentVolumeClaim.claimName === this.getName()
|
||||||
).length > 0
|
).length > 0;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStorage(): string {
|
getStorage(): string {
|
||||||
if (!this.spec.resources || !this.spec.resources.requests) return "-";
|
if (!this.spec.resources || !this.spec.resources.requests) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
return this.spec.resources.requests.storage;
|
return this.spec.resources.requests.storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchLabels(): string[] {
|
getMatchLabels(): string[] {
|
||||||
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
|
if (!this.spec.selector || !this.spec.selector.matchLabels) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return Object.entries(this.spec.selector.matchLabels)
|
return Object.entries(this.spec.selector.matchLabels)
|
||||||
.map(([name, val]) => `${name}:${val}`);
|
.map(([name, val]) => `${name}:${val}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchExpressions() {
|
getMatchExpressions(): MatchExpression[] {
|
||||||
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
|
return this.spec.selector?.matchExpressions;
|
||||||
return this.spec.selector.matchExpressions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): string {
|
getStatus(): string {
|
||||||
if (this.status) return this.status.phase;
|
if (this.status) {
|
||||||
return "-"
|
return this.status.phase;
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,23 +43,23 @@ export class PersistentVolume extends KubeObject {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCapacity(inBytes = false) {
|
getCapacity(inBytes = false): number | string {
|
||||||
const capacity = this.spec.capacity;
|
const capacity = this.spec.capacity;
|
||||||
if (capacity) {
|
if (capacity) {
|
||||||
if (inBytes) return unitsToBytes(capacity.storage)
|
if (inBytes) {
|
||||||
|
return unitsToBytes(capacity.storage);
|
||||||
|
}
|
||||||
return capacity.storage;
|
return capacity.storage;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus(): string {
|
||||||
if (!this.status) return;
|
return this?.status.phase || "-";
|
||||||
return this.status.phase || "-";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getClaimRefName() {
|
getClaimRefName(): string {
|
||||||
const { claimRef } = this.spec;
|
return this.spec.claimRef.name;
|
||||||
return claimRef ? claimRef.name : "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { Metrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class PodsApi extends KubeApi<Pod> {
|
export class PodsApi extends KubeApi<Pod> {
|
||||||
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
|
async getLogs(params: { namespace: string; name: string }, query?: PodLogsQuery): Promise<string> {
|
||||||
const path = this.getUrl(params) + "/log";
|
const path = this.getUrl(params) + "/log";
|
||||||
return this.request.get(path, { query });
|
return this.request.get(path, { query });
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetrics(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {
|
getMetrics(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<PodMetricsData> {
|
||||||
const podSelector = pods.map(pod => pod.getName()).join("|");
|
const podSelector = pods.map(pod => pod.getName()).join("|");
|
||||||
const opts = { category: "pods", pods: podSelector, namespace, selector }
|
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||||
|
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
cpuUsage: opts,
|
cpuUsage: opts,
|
||||||
@ -29,7 +29,7 @@ export class PodsApi extends KubeApi<Pod> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPodMetrics<T = IMetrics> {
|
export interface PodMetricsData<T = Metrics> {
|
||||||
[metric: string]: T;
|
[metric: string]: T;
|
||||||
cpuUsage: T;
|
cpuUsage: T;
|
||||||
cpuRequests: T;
|
cpuRequests: T;
|
||||||
@ -42,7 +42,7 @@ export interface IPodMetrics<T = IMetrics> {
|
|||||||
networkTransmit: T;
|
networkTransmit: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPodLogsQuery {
|
export interface PodLogsQuery {
|
||||||
container?: string;
|
container?: string;
|
||||||
tailLines?: number;
|
tailLines?: number;
|
||||||
timestamps?: boolean;
|
timestamps?: boolean;
|
||||||
@ -58,7 +58,7 @@ export enum PodStatus {
|
|||||||
EVICTED = "Evicted"
|
EVICTED = "Evicted"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPodContainer {
|
export interface PodContainer {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
command?: string[];
|
command?: string[];
|
||||||
@ -106,12 +106,12 @@ export interface IPodContainer {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
mountPath: string;
|
mountPath: string;
|
||||||
}[];
|
}[];
|
||||||
livenessProbe?: IContainerProbe;
|
livenessProbe?: ContainerProbe;
|
||||||
readinessProbe?: IContainerProbe;
|
readinessProbe?: ContainerProbe;
|
||||||
imagePullPolicy: string;
|
imagePullPolicy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IContainerProbe {
|
interface ContainerProbe {
|
||||||
httpGet?: {
|
httpGet?: {
|
||||||
path?: string;
|
path?: string;
|
||||||
port: number;
|
port: number;
|
||||||
@ -131,7 +131,7 @@ interface IContainerProbe {
|
|||||||
failureThreshold?: number;
|
failureThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPodContainerStatus {
|
export interface PodContainerStatus {
|
||||||
name: string;
|
name: string;
|
||||||
state: {
|
state: {
|
||||||
[index: string]: object;
|
[index: string]: object;
|
||||||
@ -173,14 +173,14 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
};
|
};
|
||||||
configMap: {
|
configMap: {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
};
|
||||||
secret: {
|
secret: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
defaultMode: number;
|
defaultMode: number;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
initContainers: IPodContainer[];
|
initContainers: PodContainer[];
|
||||||
containers: IPodContainer[];
|
containers: PodContainer[];
|
||||||
restartPolicy: string;
|
restartPolicy: string;
|
||||||
terminationGracePeriodSeconds: number;
|
terminationGracePeriodSeconds: number;
|
||||||
dnsPolicy: string;
|
dnsPolicy: string;
|
||||||
@ -200,7 +200,7 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
effect: string;
|
effect: string;
|
||||||
tolerationSeconds: number;
|
tolerationSeconds: number;
|
||||||
}[];
|
}[];
|
||||||
affinity: IAffinity;
|
affinity: Affinity;
|
||||||
}
|
}
|
||||||
status: {
|
status: {
|
||||||
phase: string;
|
phase: string;
|
||||||
@ -213,34 +213,29 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
hostIP: string;
|
hostIP: string;
|
||||||
podIP: string;
|
podIP: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
initContainerStatuses?: IPodContainerStatus[];
|
initContainerStatuses?: PodContainerStatus[];
|
||||||
containerStatuses?: IPodContainerStatus[];
|
containerStatuses?: PodContainerStatus[];
|
||||||
qosClass: string;
|
qosClass: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitContainers() {
|
getAllContainers(): PodContainer[] {
|
||||||
return this.spec.initContainers || [];
|
return this.spec.containers.concat(this.spec.initContainers);
|
||||||
}
|
}
|
||||||
|
|
||||||
getContainers() {
|
getRunningContainers(): PodContainer[] {
|
||||||
return this.spec.containers || [];
|
const activeContainers = new Set(
|
||||||
|
this.getContainerStatuses()
|
||||||
|
.filter(({ state }) => !!state.running)
|
||||||
|
.map(({ name }) => name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.getAllContainers()
|
||||||
|
.filter(({ name }) => activeContainers.has(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllContainers() {
|
getContainerStatuses(includeInitContainers = true): PodContainerStatus[] {
|
||||||
return this.getContainers().concat(this.getInitContainers());
|
const statuses: PodContainerStatus[] = [];
|
||||||
}
|
|
||||||
|
|
||||||
getRunningContainers() {
|
|
||||||
const statuses = this.getContainerStatuses()
|
|
||||||
return this.getAllContainers().filter(container => {
|
|
||||||
return statuses.find(status => status.name === container.name && !!status.state["running"])
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getContainerStatuses(includeInitContainers = true) {
|
|
||||||
const statuses: IPodContainerStatus[] = [];
|
|
||||||
const { containerStatuses, initContainerStatuses } = this.status;
|
const { containerStatuses, initContainerStatuses } = this.status;
|
||||||
if (containerStatuses) {
|
if (containerStatuses) {
|
||||||
statuses.push(...containerStatuses);
|
statuses.push(...containerStatuses);
|
||||||
@ -253,28 +248,22 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
|
|
||||||
getRestartsCount(): number {
|
getRestartsCount(): number {
|
||||||
const { containerStatuses } = this.status;
|
const { containerStatuses } = this.status;
|
||||||
if (!containerStatuses) return 0;
|
if (!containerStatuses) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return containerStatuses.reduce((count, item) => count + item.restartCount, 0);
|
return containerStatuses.reduce((count, item) => count + item.restartCount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQosClass() {
|
getReason(): string {
|
||||||
return this.status.qosClass || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getReason() {
|
|
||||||
return this.status.reason || "";
|
return this.status.reason || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getPriorityClassName() {
|
|
||||||
return this.spec.priorityClassName || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns one of 5 statuses: Running, Succeeded, Pending, Failed, Evicted
|
// Returns one of 5 statuses: Running, Succeeded, Pending, Failed, Evicted
|
||||||
getStatus() {
|
getStatus(): PodStatus {
|
||||||
const phase = this.getStatusPhase();
|
const phase = this.status.phase;
|
||||||
const reason = this.getReason();
|
const reason = this.getReason();
|
||||||
const goodConditions = ["Initialized", "Ready"].every(condition =>
|
const goodConditions = ["Initialized", "Ready"].every(condition =>
|
||||||
!!this.getConditions().find(item => item.type === condition && item.status === "True")
|
!!this.status.conditions.find(item => item.type === condition && item.status === "True")
|
||||||
);
|
);
|
||||||
if (reason === PodStatus.EVICTED) {
|
if (reason === PodStatus.EVICTED) {
|
||||||
return PodStatus.EVICTED;
|
return PodStatus.EVICTED;
|
||||||
@ -292,9 +281,13 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns pod phase or container error if occured
|
// Returns pod phase or container error if occured
|
||||||
getStatusMessage() {
|
getStatusMessage(): string {
|
||||||
if (this.getReason() === PodStatus.EVICTED) return "Evicted";
|
if (this.getReason() === PodStatus.EVICTED) {
|
||||||
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) return "Terminating";
|
return "Evicted";
|
||||||
|
}
|
||||||
|
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) {
|
||||||
|
return "Terminating";
|
||||||
|
}
|
||||||
|
|
||||||
let message = "";
|
let message = "";
|
||||||
const statuses = this.getContainerStatuses(false); // not including initContainers
|
const statuses = this.getContainerStatuses(false); // not including initContainers
|
||||||
@ -309,74 +302,61 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
const { reason } = state.terminated;
|
const { reason } = state.terminated;
|
||||||
message = reason ? reason : "Terminated";
|
message = reason ? reason : "Terminated";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
if (message) return message;
|
|
||||||
return this.getStatusPhase();
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusPhase() {
|
|
||||||
return this.status.phase;
|
return this.status.phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
|
||||||
return this.status.conditions || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getVolumes() {
|
|
||||||
return this.spec.volumes || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getSecrets(): string[] {
|
getSecrets(): string[] {
|
||||||
return this.getVolumes()
|
return this.spec.volumes
|
||||||
.filter(vol => vol.secret)
|
.filter(vol => vol.secret)
|
||||||
.map(vol => vol.secret.secretName);
|
.map(vol => vol.secret.secretName);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeSelectors(): string[] {
|
getNodeSelectors(): string[] {
|
||||||
const { nodeSelector } = this.spec
|
const { nodeSelector } = this.spec;
|
||||||
if (!nodeSelector) return []
|
if (!nodeSelector) {
|
||||||
return Object.entries(nodeSelector).map(values => values.join(": "))
|
return [];
|
||||||
|
}
|
||||||
|
return Object.entries(nodeSelector).map(values => values.join(": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTolerations() {
|
hasIssues(): boolean {
|
||||||
return this.spec.tolerations || []
|
const notReady = !!this.status.conditions.find(condition => {
|
||||||
}
|
return condition.type == "Ready" && condition.status !== "True";
|
||||||
|
|
||||||
getAffinity(): IAffinity {
|
|
||||||
return this.spec.affinity
|
|
||||||
}
|
|
||||||
|
|
||||||
hasIssues() {
|
|
||||||
const notReady = !!this.getConditions().find(condition => {
|
|
||||||
return condition.type == "Ready" && condition.status !== "True"
|
|
||||||
});
|
});
|
||||||
const crashLoop = !!this.getContainerStatuses().find(condition => {
|
const crashLoop = !!this.getContainerStatuses().find(condition => {
|
||||||
const waiting = condition.state.waiting
|
const waiting = condition.state.waiting;
|
||||||
return (waiting && waiting.reason == "CrashLoopBackOff")
|
return (waiting && waiting.reason == "CrashLoopBackOff");
|
||||||
})
|
});
|
||||||
return (
|
return (
|
||||||
notReady ||
|
notReady ||
|
||||||
crashLoop ||
|
crashLoop ||
|
||||||
this.getStatusPhase() !== "Running"
|
this.status.phase !== "Running"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLivenessProbe(container: IPodContainer) {
|
getLivenessProbe(container: PodContainer): string[] {
|
||||||
return this.getProbe(container.livenessProbe);
|
return this.getProbe(container.livenessProbe);
|
||||||
}
|
}
|
||||||
|
|
||||||
getReadinessProbe(container: IPodContainer) {
|
getReadinessProbe(container: PodContainer): string[] {
|
||||||
return this.getProbe(container.readinessProbe);
|
return this.getProbe(container.readinessProbe);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProbe(probeData: IContainerProbe) {
|
getProbe(probeData: ContainerProbe): string[] {
|
||||||
if (!probeData) return [];
|
if (!probeData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
|
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
|
||||||
periodSeconds, successThreshold, failureThreshold
|
periodSeconds, successThreshold, failureThreshold
|
||||||
} = probeData;
|
} = probeData;
|
||||||
const probe = [];
|
const probe: string[] = [];
|
||||||
// HTTP Request
|
// HTTP Request
|
||||||
if (httpGet) {
|
if (httpGet) {
|
||||||
const { path, port, host, scheme } = httpGet;
|
const { path, port, host, scheme } = httpGet;
|
||||||
@ -403,15 +383,8 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
return probe;
|
return probe;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeName() {
|
getSelectedNodeOs(): string | null {
|
||||||
return this.spec?.nodeName
|
return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"] || null;
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedNodeOs() {
|
|
||||||
if (!this.spec.nodeSelector) return
|
|
||||||
if (!this.spec.nodeSelector["kubernetes.io/os"] && !this.spec.nodeSelector["beta.kubernetes.io/os"]) return
|
|
||||||
|
|
||||||
return this.spec.nodeSelector["kubernetes.io/os"] || this.spec.nodeSelector["beta.kubernetes.io/os"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,14 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
|
export interface Rules {
|
||||||
|
fsGroup: string;
|
||||||
|
runAsGroup: string;
|
||||||
|
runAsUser: string;
|
||||||
|
supplementalGroups: string;
|
||||||
|
seLinux: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class PodSecurityPolicy extends KubeObject {
|
export class PodSecurityPolicy extends KubeObject {
|
||||||
static kind = "PodSecurityPolicy"
|
static kind = "PodSecurityPolicy"
|
||||||
@ -66,22 +74,22 @@ export class PodSecurityPolicy extends KubeObject {
|
|||||||
volumes?: string[];
|
volumes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrivileged() {
|
isPrivileged(): boolean {
|
||||||
return !!this.spec.privileged;
|
return !!this.spec.privileged;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVolumes() {
|
getVolumes(): string[] {
|
||||||
return this.spec.volumes || [];
|
return this.spec.volumes || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRules() {
|
getRules(): Rules {
|
||||||
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
|
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
|
||||||
return {
|
return {
|
||||||
fsGroup: fsGroup ? fsGroup.rule : "",
|
fsGroup: fsGroup?.rule || "",
|
||||||
runAsGroup: runAsGroup ? runAsGroup.rule : "",
|
runAsGroup: runAsGroup?.rule || "",
|
||||||
runAsUser: runAsUser ? runAsUser.rule : "",
|
runAsUser: runAsUser?.rule || "",
|
||||||
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
|
supplementalGroups: supplementalGroups?.rule || "",
|
||||||
seLinux: seLinux ? seLinux.rule : "",
|
seLinux: seLinux?.rule || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { IPodContainer } from "./pods.api";
|
import { PodContainer } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
@ -15,10 +15,10 @@ export class ReplicaSet extends WorkloadKubeObject {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
containers?: IPodContainer[];
|
containers?: PodContainer[];
|
||||||
template?: {
|
template?: {
|
||||||
spec?: {
|
spec?: {
|
||||||
affinity?: IAffinity;
|
affinity?: Affinity;
|
||||||
nodeSelector?: {
|
nodeSelector?: {
|
||||||
[selector: string]: string;
|
[selector: string]: string;
|
||||||
};
|
};
|
||||||
@ -28,7 +28,7 @@ export class ReplicaSet extends WorkloadKubeObject {
|
|||||||
effect: string;
|
effect: string;
|
||||||
tolerationSeconds: number;
|
tolerationSeconds: number;
|
||||||
}[];
|
}[];
|
||||||
containers: IPodContainer[];
|
containers: PodContainer[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
restartPolicy?: string;
|
restartPolicy?: string;
|
||||||
@ -44,9 +44,9 @@ export class ReplicaSet extends WorkloadKubeObject {
|
|||||||
observedGeneration: number;
|
observedGeneration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages(): string[] {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
|
const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||||
return [...containers].map(container => container.image)
|
return containers.map(container => container.image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import jsYaml from "js-yaml"
|
import jsYaml from "js-yaml";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
import { KubeJsonApiData } from "../kube-json-api";
|
||||||
import { apiKubeResourceApplier } from "../index";
|
import { apiKubeResourceApplier } from "../index";
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { KubeObject } from "../kube-object";
|
|||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
import { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
export interface IResourceQuotaValues {
|
export interface ResourceQuotaValues {
|
||||||
[quota: string]: string;
|
[quota: string]: string;
|
||||||
|
|
||||||
// Compute Resource Quota
|
// Compute Resource Quota
|
||||||
@ -30,33 +30,34 @@ export interface IResourceQuotaValues {
|
|||||||
"count/deployments.extensions"?: string;
|
"count/deployments.extensions"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MatchExpression {
|
||||||
|
operator: string;
|
||||||
|
scopeName: string;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class ResourceQuota extends KubeObject {
|
export class ResourceQuota extends KubeObject {
|
||||||
static kind = "ResourceQuota"
|
static kind = "ResourceQuota"
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
constructor(data: KubeJsonApiData) {
|
||||||
super(data);
|
super(data);
|
||||||
this.spec = this.spec || {} as any
|
this.spec = this.spec || {} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
spec: {
|
spec: {
|
||||||
hard: IResourceQuotaValues;
|
hard: ResourceQuotaValues;
|
||||||
scopeSelector?: {
|
scopeSelector?: {
|
||||||
matchExpressions: {
|
matchExpressions: MatchExpression[];
|
||||||
operator: string;
|
|
||||||
scopeName: string;
|
|
||||||
values: string[];
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
status: {
|
status: {
|
||||||
hard: IResourceQuotaValues;
|
hard: ResourceQuotaValues;
|
||||||
used: IResourceQuotaValues;
|
used: ResourceQuotaValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScopeSelector() {
|
getScopeSelector(): MatchExpression[] {
|
||||||
const { matchExpressions = [] } = this.spec.scopeSelector || {};
|
return this.spec?.scopeSelector?.matchExpressions;
|
||||||
return matchExpressions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export interface IRoleBindingSubject {
|
export interface RoleBindingSubject {
|
||||||
kind: string;
|
kind: string;
|
||||||
name: string;
|
name: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
@ -13,19 +13,19 @@ export interface IRoleBindingSubject {
|
|||||||
export class RoleBinding extends KubeObject {
|
export class RoleBinding extends KubeObject {
|
||||||
static kind = "RoleBinding"
|
static kind = "RoleBinding"
|
||||||
|
|
||||||
subjects?: IRoleBindingSubject[]
|
subjects?: RoleBindingSubject[]
|
||||||
roleRef: {
|
roleRef: {
|
||||||
kind: string;
|
kind: string;
|
||||||
name: string;
|
name: string;
|
||||||
apiGroup?: string;
|
apiGroup?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubjects() {
|
getSubjects(): RoleBindingSubject[] {
|
||||||
return this.subjects || [];
|
return this.subjects || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubjectNames(): string {
|
getSubjectNames(): string {
|
||||||
return this.getSubjects().map(subject => subject.name).join(", ")
|
return this.getSubjects().map(subject => subject.name).join(", ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
|
export interface Rule {
|
||||||
|
verbs: string[];
|
||||||
|
apiGroups: string[];
|
||||||
|
resources: string[];
|
||||||
|
resourceNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class Role extends KubeObject {
|
export class Role extends KubeObject {
|
||||||
static kind = "Role"
|
static kind = "Role"
|
||||||
|
|
||||||
rules: {
|
rules: Rule[]
|
||||||
verbs: string[];
|
|
||||||
apiGroups: string[];
|
|
||||||
resources: string[];
|
|
||||||
resourceNames?: string[];
|
|
||||||
}[]
|
|
||||||
|
|
||||||
getRules() {
|
getRules(): Rule[] {
|
||||||
return this.rules || [];
|
return this.rules;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export enum SecretType {
|
|||||||
BootstrapToken = "bootstrap.kubernetes.io/token",
|
BootstrapToken = "bootstrap.kubernetes.io/token",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISecretRef {
|
export interface SecretRef {
|
||||||
key?: string;
|
key?: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@ -37,10 +37,6 @@ export class Secret extends KubeObject {
|
|||||||
getKeys(): string[] {
|
getKeys(): string[] {
|
||||||
return Object.keys(this.data);
|
return Object.keys(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken() {
|
|
||||||
return this.data.token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const secretsApi = new KubeApi({
|
export const secretsApi = new KubeApi({
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISelfSubjectReviewRule {
|
export interface SelfSubjectReviewRule {
|
||||||
verbs: string[];
|
verbs: string[];
|
||||||
apiGroups?: string[];
|
apiGroups?: string[];
|
||||||
resources?: string[];
|
resources?: string[];
|
||||||
@ -29,22 +29,22 @@ export class SelfSubjectRulesReview extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status: {
|
status: {
|
||||||
resourceRules: ISelfSubjectReviewRule[];
|
resourceRules: SelfSubjectReviewRule[];
|
||||||
nonResourceRules: ISelfSubjectReviewRule[];
|
nonResourceRules: SelfSubjectReviewRule[];
|
||||||
incomplete: boolean;
|
incomplete: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceRules() {
|
getResourceRules(): SelfSubjectReviewRule[] {
|
||||||
const rules = this.status && this.status.resourceRules || [];
|
const rules = this.status && this.status.resourceRules || [];
|
||||||
return rules.map(rule => this.normalize(rule));
|
return rules.map(rule => this.normalize(rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
getNonResourceRules() {
|
getNonResourceRules(): SelfSubjectReviewRule[] {
|
||||||
const rules = this.status && this.status.nonResourceRules || [];
|
const rules = this.status && this.status.nonResourceRules || [];
|
||||||
return rules.map(rule => this.normalize(rule));
|
return rules.map(rule => this.normalize(rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
|
protected normalize(rule: SelfSubjectReviewRule): SelfSubjectReviewRule {
|
||||||
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
|
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
|
||||||
return {
|
return {
|
||||||
apiGroups,
|
apiGroups,
|
||||||
@ -56,7 +56,7 @@ export class SelfSubjectRulesReview extends KubeObject {
|
|||||||
const separator = apiGroup == "" ? "" : ".";
|
const separator = apiGroup == "" ? "" : ".";
|
||||||
return resource + separator + apiGroup;
|
return resource + separator + apiGroup;
|
||||||
})
|
})
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,22 +2,26 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
|
export interface ImagePullSecret {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Secret {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ServiceAccount extends KubeObject {
|
export class ServiceAccount extends KubeObject {
|
||||||
static kind = "ServiceAccount";
|
static kind = "ServiceAccount";
|
||||||
|
|
||||||
secrets?: {
|
secrets?: Secret[]
|
||||||
name: string;
|
imagePullSecrets?: ImagePullSecret[]
|
||||||
}[]
|
|
||||||
imagePullSecrets?: {
|
|
||||||
name: string;
|
|
||||||
}[]
|
|
||||||
|
|
||||||
getSecrets() {
|
getSecrets(): Secret[] {
|
||||||
return this.secrets || [];
|
return this.secrets || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getImagePullSecrets() {
|
getImagePullSecrets(): ImagePullSecret[] {
|
||||||
return this.imagePullSecrets || [];
|
return this.imagePullSecrets || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,25 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export interface IServicePort {
|
export interface ServicePort {
|
||||||
name?: string;
|
name?: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
port: number;
|
port: number;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServicePort implements IServicePort {
|
export class ServicePort implements ServicePort {
|
||||||
name?: string;
|
name?: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
port: number;
|
port: number;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
nodePort?: number;
|
nodePort?: number;
|
||||||
|
|
||||||
constructor(data: IServicePort) {
|
constructor(data: ServicePort) {
|
||||||
Object.assign(this, data)
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString(): string {
|
||||||
if (this.nodePort) {
|
if (this.nodePort) {
|
||||||
return `${this.port}:${this.nodePort}/${this.protocol}`;
|
return `${this.port}:${this.nodePort}/${this.protocol}`;
|
||||||
} else {
|
} else {
|
||||||
@ -29,6 +29,13 @@ export class ServicePort implements IServicePort {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadBalancer {
|
||||||
|
ingress?: {
|
||||||
|
ip?: string;
|
||||||
|
hostname?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Service extends KubeObject {
|
export class Service extends KubeObject {
|
||||||
static kind = "Service"
|
static kind = "Service"
|
||||||
@ -45,32 +52,23 @@ export class Service extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status: {
|
status: {
|
||||||
loadBalancer?: {
|
loadBalancer?: LoadBalancer;
|
||||||
ingress?: {
|
|
||||||
ip?: string;
|
|
||||||
hostname?: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getClusterIp() {
|
getExternalIps(): string[] {
|
||||||
return this.spec.clusterIP;
|
return this.status.loadBalancer?.ingress?.map((val): string => val.ip || val.hostname || "")
|
||||||
|
|| this.spec.externalIPs
|
||||||
|
|| [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getExternalIps() {
|
getType(): string {
|
||||||
const lb = this.getLoadBalancer();
|
|
||||||
if (lb && lb.ingress) {
|
|
||||||
return lb.ingress.map(val => val.ip || val.hostname)
|
|
||||||
}
|
|
||||||
return this.spec.externalIPs || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getType() {
|
|
||||||
return this.spec.type || "-";
|
return this.spec.type || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelector(): string[] {
|
getSelector(): string[] {
|
||||||
if (!this.spec.selector) return [];
|
if (!this.spec.selector) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return Object.entries(this.spec.selector).map(val => val.join("="));
|
return Object.entries(this.spec.selector).map(val => val.join("="));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,15 +77,11 @@ export class Service extends KubeObject {
|
|||||||
return ports.map(p => new ServicePort(p));
|
return ports.map(p => new ServicePort(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoadBalancer() {
|
isActive(): boolean {
|
||||||
return this.status.loadBalancer;
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive() {
|
|
||||||
return this.getType() !== "LoadBalancer" || this.getExternalIps().length > 0;
|
return this.getType() !== "LoadBalancer" || this.getExternalIps().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus(): "Active" | "Pending" {
|
||||||
return this.isActive() ? "Active" : "Pending";
|
return this.isActive() ? "Active" : "Pending";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { IPodContainer } from "./pods.api";
|
import { PodContainer } from "./pods.api";
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export class StatefulSet extends WorkloadKubeObject {
|
|||||||
mountPath: string;
|
mountPath: string;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
affinity?: IAffinity;
|
affinity?: Affinity;
|
||||||
nodeSelector?: {
|
nodeSelector?: {
|
||||||
[selector: string]: string;
|
[selector: string]: string;
|
||||||
};
|
};
|
||||||
@ -70,9 +70,9 @@ export class StatefulSet extends WorkloadKubeObject {
|
|||||||
collisionCount: number;
|
collisionCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages(): string[] {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
|
const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||||
return [...containers].map(container => container.image)
|
return containers.map(container => container.image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,20 +14,20 @@ export class StorageClass extends KubeObject {
|
|||||||
[param: string]: string; // every provisioner has own set of these parameters
|
[param: string]: string; // every provisioner has own set of these parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
isDefault() {
|
isDefault(): boolean {
|
||||||
const annotations = this.metadata.annotations || {};
|
const annotations = this.metadata.annotations || {};
|
||||||
return (
|
return (
|
||||||
annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
||||||
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVolumeBindingMode() {
|
getVolumeBindingMode(): string {
|
||||||
return this.volumeBindingMode || "-"
|
return this.volumeBindingMode || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
getReclaimPolicy() {
|
getReclaimPolicy(): string {
|
||||||
return this.reclaimPolicy || "-"
|
return this.reclaimPolicy || "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const apiKubeResourceApplier = new KubeJsonApi({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Common handler for HTTP api errors
|
// Common handler for HTTP api errors
|
||||||
function onApiError(error: JsonApiErrorParsed, res: Response) {
|
function onApiError(error: JsonApiErrorParsed, res: Response): void {
|
||||||
switch (res.status) {
|
switch (res.status) {
|
||||||
case 403:
|
case 403:
|
||||||
error.isUsedForNotification = true;
|
error.isUsedForNotification = true;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { EventEmitter } from "../utils/eventEmitter";
|
import { EventEmitter } from "../utils/eventEmitter";
|
||||||
import { cancelableFetch } from "../utils/cancelableFetch";
|
import { cancelableFetch, CancelablePromise } from "../utils/cancelableFetch";
|
||||||
|
|
||||||
export interface JsonApiData {
|
export interface JsonApiData {
|
||||||
}
|
}
|
||||||
@ -31,6 +31,21 @@ export interface JsonApiConfig {
|
|||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class JsonApiErrorParsed {
|
||||||
|
isUsedForNotification = false;
|
||||||
|
|
||||||
|
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAborted(): boolean {
|
||||||
|
return this.error.code === DOMException.ABORT_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.messages.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||||
static reqInitDefault: RequestInit = {
|
static reqInitDefault: RequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
@ -51,30 +66,30 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
public onData = new EventEmitter<[D, Response]>();
|
public onData = new EventEmitter<[D, Response]>();
|
||||||
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
||||||
|
|
||||||
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
||||||
}
|
}
|
||||||
|
|
||||||
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "post" });
|
return this.request<T>(path, params, { ...reqInit, method: "post" });
|
||||||
}
|
}
|
||||||
|
|
||||||
put<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
put<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "put" });
|
return this.request<T>(path, params, { ...reqInit, method: "put" });
|
||||||
}
|
}
|
||||||
|
|
||||||
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "patch" });
|
return this.request<T>(path, params, { ...reqInit, method: "patch" });
|
||||||
}
|
}
|
||||||
|
|
||||||
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected request<D>(path: string, params?: P, init: RequestInit = {}) {
|
protected request<D>(path: string, params?: P, init: RequestInit = {}): CancelablePromise<D> {
|
||||||
let reqUrl = this.config.apiPrefix + path;
|
let reqUrl = this.config.apiPrefix + path;
|
||||||
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
||||||
const { data, query } = params || {} as P;
|
const { data, query } = params || {};
|
||||||
if (data && !reqInit.body) {
|
if (data && !reqInit.body) {
|
||||||
reqInit.body = JSON.stringify(data);
|
reqInit.body = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
@ -110,46 +125,35 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
} else {
|
} else {
|
||||||
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
||||||
this.onError.emit(error, res);
|
this.onError.emit(error, res);
|
||||||
this.writeLog({ ...log, error })
|
this.writeLog({ ...log, error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
return [error]
|
return [error];
|
||||||
|
} else if (Array.isArray(error.errors)) {
|
||||||
|
return error.errors.map(error => error.title);
|
||||||
|
} else if (error.message) {
|
||||||
|
return [error.message];
|
||||||
}
|
}
|
||||||
else if (Array.isArray(error.errors)) {
|
return [res.statusText || "Error!"];
|
||||||
return error.errors.map(error => error.title)
|
|
||||||
}
|
|
||||||
else if (error.message) {
|
|
||||||
return [error.message]
|
|
||||||
}
|
|
||||||
return [res.statusText || "Error!"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeLog(log: JsonApiLog) {
|
protected writeLog(log: JsonApiLog): void {
|
||||||
if (!this.config.debug) return;
|
if (!this.config.debug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { method, reqUrl, ...params } = log;
|
const { method, reqUrl, ...params } = log;
|
||||||
let textStyle = 'font-weight: bold;';
|
let textStyle = 'font-weight: bold;';
|
||||||
if (params.data) textStyle += 'background: green; color: white;';
|
if (params.data) {
|
||||||
if (params.error) textStyle += 'background: red; color: white;';
|
textStyle += 'background: green; color: white;';
|
||||||
|
}
|
||||||
|
if (params.error) {
|
||||||
|
textStyle += 'background: red; color: white;';
|
||||||
|
}
|
||||||
console.log(`%c${method} ${reqUrl}`, textStyle, params);
|
console.log(`%c${method} ${reqUrl}`, textStyle, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JsonApiErrorParsed {
|
|
||||||
isUsedForNotification = false;
|
|
||||||
|
|
||||||
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAborted() {
|
|
||||||
return this.error.code === DOMException.ABORT_ERR;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.messages.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
// Base class for building all kubernetes apis
|
// Base class for building all kubernetes apis
|
||||||
|
|
||||||
import merge from "lodash/merge"
|
import merge from "lodash/merge";
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { IKubeObjectConstructor, KubeObject } from "./kube-object";
|
import { IKubeObjectConstructor, KubeObject } from "./kube-object";
|
||||||
import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
import { KubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
||||||
import { apiKube } from "./index";
|
import { apiKube } from "./index";
|
||||||
import { kubeWatchApi } from "./kube-watch-api";
|
import { kubeWatchApi } from "./kube-watch-api";
|
||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
import { split } from "../utils/arrays";
|
import { split } from "../utils/arrays";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
|
||||||
export interface IKubeApiOptions<T extends KubeObject> {
|
export interface KubeApiOptions<T extends KubeObject> {
|
||||||
kind: string; // resource type within api-group, e.g. "Namespace"
|
kind: string; // resource type within api-group, e.g. "Namespace"
|
||||||
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
|
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||||
isNamespaced: boolean;
|
isNamespaced: boolean;
|
||||||
@ -17,7 +18,7 @@ export interface IKubeApiOptions<T extends KubeObject> {
|
|||||||
request?: KubeJsonApi;
|
request?: KubeJsonApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeApiQueryParams {
|
export interface KubeApiQueryParams {
|
||||||
watch?: boolean | number;
|
watch?: boolean | number;
|
||||||
resourceVersion?: string;
|
resourceVersion?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
@ -25,7 +26,7 @@ export interface IKubeApiQueryParams {
|
|||||||
continue?: string; // might be used with ?limit from second request
|
continue?: string; // might be used with ?limit from second request
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeApiLinkRef {
|
export interface KubeApiLinkRef {
|
||||||
apiPrefix?: string;
|
apiPrefix?: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
resource: string;
|
resource: string;
|
||||||
@ -33,14 +34,14 @@ export interface IKubeApiLinkRef {
|
|||||||
namespace?: string;
|
namespace?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
|
export interface KubeApiLinkBase extends KubeApiLinkRef {
|
||||||
apiBase: string;
|
apiBase: string;
|
||||||
apiGroup: string;
|
apiGroup: string;
|
||||||
apiVersionWithGroup: string;
|
apiVersionWithGroup: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubeApi<T extends KubeObject = any> {
|
export class KubeApi<T extends KubeObject = any> {
|
||||||
static parseApi(apiPath = ""): IKubeApiLinkBase {
|
static parseApi(apiPath = ""): KubeApiLinkBase {
|
||||||
apiPath = new URL(apiPath, location.origin).pathname;
|
apiPath = new URL(apiPath, location.origin).pathname;
|
||||||
const [, prefix, ...parts] = apiPath.split("/");
|
const [, prefix, ...parts] = apiPath.split("/");
|
||||||
const apiPrefix = `/${prefix}`;
|
const apiPrefix = `/${prefix}`;
|
||||||
@ -91,11 +92,11 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
*/
|
*/
|
||||||
if (left[0].includes('.') || left[1].match(/^v[0-9]/)) {
|
if (left[0].includes('.') || left[1].match(/^v[0-9]/)) {
|
||||||
[apiGroup, apiVersion] = left;
|
[apiGroup, apiVersion] = left;
|
||||||
resource = left.slice(2).join("/")
|
resource = left.slice(2).join("/");
|
||||||
} else {
|
} else {
|
||||||
apiGroup = "";
|
apiGroup = "";
|
||||||
apiVersion = left[0];
|
apiVersion = left[0];
|
||||||
[resource, name] = left.slice(1)
|
[resource, name] = left.slice(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -105,7 +106,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||||
|
|
||||||
if (!apiBase) {
|
if (!apiBase) {
|
||||||
throw new Error(`invalid apiPath: ${apiPath}`)
|
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -116,20 +117,20 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static createLink(ref: IKubeApiLinkRef): string {
|
static createLink(ref: KubeApiLinkRef): string {
|
||||||
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
|
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
|
||||||
let { namespace } = ref;
|
let { namespace } = ref;
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
namespace = `namespaces/${namespace}`
|
namespace = `namespaces/${namespace}`;
|
||||||
}
|
}
|
||||||
return [apiPrefix, apiVersion, namespace, resource, name]
|
return [apiPrefix, apiVersion, namespace, resource, name]
|
||||||
.filter(v => v)
|
.filter(v => v)
|
||||||
.join("/")
|
.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
static watchAll(...apis: KubeApi[]) {
|
static watchAll(...apis: KubeApi[]): () => void {
|
||||||
const disposers = apis.map(api => api.watch());
|
const disposers = apis.map(api => api.watch());
|
||||||
return () => disposers.forEach(unwatch => unwatch());
|
return (): void => disposers.forEach(unwatch => unwatch());
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly kind: string
|
readonly kind: string
|
||||||
@ -145,7 +146,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
protected request: KubeJsonApi;
|
protected request: KubeJsonApi;
|
||||||
protected resourceVersions = new Map<string, string>();
|
protected resourceVersions = new Map<string, string>();
|
||||||
|
|
||||||
constructor(protected options: IKubeApiOptions<T>) {
|
constructor(protected options: KubeApiOptions<T>) {
|
||||||
const {
|
const {
|
||||||
kind,
|
kind,
|
||||||
isNamespaced = false,
|
isNamespaced = false,
|
||||||
@ -169,19 +170,19 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
apiManager.registerApi(apiBase, this);
|
apiManager.registerApi(apiBase, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setResourceVersion(namespace = "", newVersion: string) {
|
setResourceVersion(namespace = "", newVersion: string): void {
|
||||||
this.resourceVersions.set(namespace, newVersion);
|
this.resourceVersions.set(namespace, newVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceVersion(namespace = "") {
|
getResourceVersion(namespace = ""): string {
|
||||||
return this.resourceVersions.get(namespace);
|
return this.resourceVersions.get(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshResourceVersion(params?: { namespace: string }) {
|
async refreshResourceVersion(params?: { namespace: string }): Promise<T[]> {
|
||||||
return this.list(params, { limit: 1 });
|
return this.list(params, { limit: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) {
|
getUrl({ name = "", namespace = "" } = {}, query?: Partial<KubeApiQueryParams>): string {
|
||||||
const { apiPrefix, apiVersionWithGroup, apiResource } = this;
|
const { apiPrefix, apiVersionWithGroup, apiResource } = this;
|
||||||
const resourcePath = KubeApi.createLink({
|
const resourcePath = KubeApi.createLink({
|
||||||
apiPrefix: apiPrefix,
|
apiPrefix: apiPrefix,
|
||||||
@ -208,7 +209,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
kind: this.kind,
|
kind: this.kind,
|
||||||
apiVersion: apiVersion,
|
apiVersion: apiVersion,
|
||||||
...item,
|
...item,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// custom apis might return array for list response, e.g. users, groups, etc.
|
// custom apis might return array for list response, e.g. users, groups, etc.
|
||||||
@ -219,13 +220,13 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise<T[]> {
|
async list({ namespace = "" } = {}, query?: KubeApiQueryParams): Promise<T[]> {
|
||||||
return this.request
|
return this.request
|
||||||
.get(this.getUrl({ namespace }), { query })
|
.get(this.getUrl({ namespace }), { query })
|
||||||
.then(data => this.parseResponse(data, namespace));
|
.then(data => this.parseResponse(data, namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T> {
|
async get({ name = "", namespace = "default" } = {}, query?: KubeApiQueryParams): Promise<T> {
|
||||||
return this.request
|
return this.request
|
||||||
.get(this.getUrl({ namespace, name }), { query })
|
.get(this.getUrl({ namespace, name }), { query })
|
||||||
.then(this.parseResponse);
|
.then(this.parseResponse);
|
||||||
@ -252,20 +253,20 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl({ namespace, name });
|
||||||
return this.request
|
return this.request
|
||||||
.put(apiUrl, { data })
|
.put(apiUrl, { data })
|
||||||
.then(this.parseResponse)
|
.then(this.parseResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete({ name = "", namespace = "default" }) {
|
delete({ name = "", namespace = "default" }): CancelablePromise<KubeJsonApiData> {
|
||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl({ namespace, name });
|
||||||
return this.request.del(apiUrl)
|
return this.request.del(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWatchUrl(namespace = "", query: IKubeApiQueryParams = {}) {
|
getWatchUrl(namespace = "", query: KubeApiQueryParams = {}): string {
|
||||||
return this.getUrl({ namespace }, {
|
return this.getUrl({ namespace }, {
|
||||||
watch: 1,
|
watch: 1,
|
||||||
resourceVersion: this.getResourceVersion(namespace),
|
resourceVersion: this.getResourceVersion(namespace),
|
||||||
...query,
|
...query,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(): () => void {
|
watch(): () => void {
|
||||||
@ -273,16 +274,16 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
|
export function lookupApiLink(ref: KubeObjectRef, parentObject: KubeObject): string {
|
||||||
const {
|
const {
|
||||||
kind, apiVersion, name,
|
kind, apiVersion, name,
|
||||||
namespace = parentObject.getNs()
|
namespace = parentObject.getNs()
|
||||||
} = ref;
|
} = ref;
|
||||||
|
|
||||||
// search in registered apis by 'kind' & 'apiVersion'
|
// search in registered apis by 'kind' & 'apiVersion'
|
||||||
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion)
|
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
|
||||||
if (api) {
|
if (api) {
|
||||||
return api.getUrl({ namespace, name })
|
return api.getUrl({ namespace, name });
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup api by generated resource link
|
// lookup api by generated resource link
|
||||||
@ -298,10 +299,10 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
|
|||||||
// resolve by kind only (hpa's might use refs to older versions of resources for example)
|
// resolve by kind only (hpa's might use refs to older versions of resources for example)
|
||||||
const apiByKind = apiManager.getApi(api => api.kind === kind);
|
const apiByKind = apiManager.getApi(api => api.kind === kind);
|
||||||
if (apiByKind) {
|
if (apiByKind) {
|
||||||
return apiByKind.getUrl({ name, namespace })
|
return apiByKind.getUrl({ name, namespace });
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise generate link with default prefix
|
// otherwise generate link with default prefix
|
||||||
// resource still might exists in k8s, but api is not registered in the app
|
// resource still might exists in k8s, but api is not registered in the app
|
||||||
return KubeApi.createLink({ apiVersion, name, namespace, resource })
|
return KubeApi.createLink({ apiVersion, name, namespace, resource });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export interface KubeJsonApiData extends JsonApiData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeObjectRef {
|
export interface KubeObjectRef {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -49,7 +49,7 @@ export interface KubeJsonApiError extends JsonApiError {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeJsonApiQuery {
|
export interface KubeJsonApiQuery {
|
||||||
watch?: any;
|
watch?: any;
|
||||||
resourceVersion?: string;
|
resourceVersion?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import { ItemObject } from "../item.store";
|
|||||||
import { apiKube } from "./index";
|
import { apiKube } from "./index";
|
||||||
import { JsonApiParams } from "./json-api";
|
import { JsonApiParams } from "./json-api";
|
||||||
import { resourceApplierApi } from "./endpoints/resource-applier.api";
|
import { resourceApplierApi } from "./endpoints/resource-applier.api";
|
||||||
|
import { CancelablePromise } from "client/utils/cancelableFetch";
|
||||||
|
|
||||||
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
|
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
|
||||||
kind?: string;
|
kind?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IKubeObjectMetadata {
|
export interface KubeObjectMetadata {
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
@ -44,11 +45,7 @@ export type IKubeMetaField = keyof KubeObject["metadata"];
|
|||||||
export class KubeObject implements ItemObject {
|
export class KubeObject implements ItemObject {
|
||||||
static readonly kind: string;
|
static readonly kind: string;
|
||||||
|
|
||||||
static create(data: any) {
|
static isNonSystem(item: KubeJsonApiData | KubeObject): boolean {
|
||||||
return new KubeObject(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static isNonSystem(item: KubeJsonApiData | KubeObject) {
|
|
||||||
return !item.metadata.name.startsWith("system:");
|
return !item.metadata.name.startsWith("system:");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +58,10 @@ export class KubeObject implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static stringifyLabels(labels: { [name: string]: string }): string[] {
|
static stringifyLabels(labels: { [name: string]: string }): string[] {
|
||||||
if (!labels) return [];
|
if (!labels) {
|
||||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`)
|
return [];
|
||||||
|
}
|
||||||
|
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
constructor(data: KubeJsonApiData) {
|
||||||
@ -71,31 +70,27 @@ export class KubeObject implements ItemObject {
|
|||||||
|
|
||||||
apiVersion: string
|
apiVersion: string
|
||||||
kind: string
|
kind: string
|
||||||
metadata: IKubeObjectMetadata;
|
metadata: KubeObjectMetadata;
|
||||||
|
|
||||||
get selfLink() {
|
get selfLink(): string {
|
||||||
return this.metadata.selfLink
|
return this.metadata.selfLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId(): string {
|
||||||
return this.metadata.uid;
|
return this.metadata.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceVersion() {
|
getName(): string {
|
||||||
return this.metadata.resourceVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
return this.metadata.name;
|
return this.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNs() {
|
getNs(): string | undefined {
|
||||||
// avoid "null" serialization via JSON.stringify when post data
|
// avoid "null" serialization via JSON.stringify when post data
|
||||||
return this.metadata.namespace || undefined;
|
return this.metadata.namespace || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: refactor with named arguments
|
// todo: refactor with named arguments
|
||||||
getAge(humanize = true, compact = true, fromNow = false) {
|
getAge(humanize = true, compact = true, fromNow = false): number | string {
|
||||||
if (fromNow) {
|
if (fromNow) {
|
||||||
return moment(this.metadata.creationTimestamp).fromNow();
|
return moment(this.metadata.creationTimestamp).fromNow();
|
||||||
}
|
}
|
||||||
@ -119,26 +114,26 @@ export class KubeObject implements ItemObject {
|
|||||||
return labels.filter(label => {
|
return labels.filter(label => {
|
||||||
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
|
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
|
||||||
return !skip;
|
return !skip;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getOwnerRefs() {
|
getOwnerRefs(): Required<KubeObjectMetadata["ownerReferences"]> {
|
||||||
const refs = this.metadata.ownerReferences || [];
|
const refs = this.metadata.ownerReferences || [];
|
||||||
return refs.map(ownerRef => ({
|
return refs.map(ownerRef => ({
|
||||||
...ownerRef,
|
...ownerRef,
|
||||||
namespace: this.getNs(),
|
namespace: this.getNs(),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchFields() {
|
getSearchFields(): string[] {
|
||||||
const { getName, getId, getNs, getAnnotations, getLabels } = this
|
const { getName, getId, getNs, getAnnotations, getLabels } = this;
|
||||||
return [
|
return [
|
||||||
getName(),
|
getName(),
|
||||||
getNs(),
|
getNs(),
|
||||||
getId(),
|
getId(),
|
||||||
...getLabels(),
|
...getLabels(),
|
||||||
...getAnnotations(),
|
...getAnnotations(),
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
toPlainObject(): object {
|
toPlainObject(): object {
|
||||||
@ -146,14 +141,14 @@ export class KubeObject implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// use unified resource-applier api for updating all k8s objects
|
// use unified resource-applier api for updating all k8s objects
|
||||||
async update<T extends KubeObject>(data: Partial<T>) {
|
async update<T extends KubeObject>(data: Partial<T>): Promise<T> {
|
||||||
return resourceApplierApi.update<T>({
|
return resourceApplierApi.update<T>({
|
||||||
...this.toPlainObject(),
|
...this.toPlainObject(),
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(params?: JsonApiParams) {
|
delete(params?: JsonApiParams): CancelablePromise<any> {
|
||||||
return apiKube.del(this.selfLink, params);
|
return apiKube.del(this.selfLink, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// Kubernetes watch-api consumer
|
// Kubernetes watch-api consumer
|
||||||
|
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { stringify } from "querystring"
|
import { stringify } from "querystring";
|
||||||
import { autobind, EventEmitter, interval } from "../utils";
|
import { autobind, EventEmitter, interval } from "../utils";
|
||||||
import { KubeJsonApiData } from "./kube-json-api";
|
import { KubeJsonApiData } from "./kube-json-api";
|
||||||
import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery } from "../../server/common/kubewatch";
|
import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery } from "../../server/common/kubewatch";
|
||||||
@ -12,7 +12,7 @@ import { apiManager } from "./api-manager";
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
IKubeWatchEvent
|
IKubeWatchEvent
|
||||||
}
|
};
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
@ -32,22 +32,25 @@ export class KubeWatchApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get activeApis() {
|
@computed get activeApis(): KubeApi<any>[] {
|
||||||
return Array.from(this.subscribers.keys());
|
return Array.from(this.subscribers.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribersCount(api: KubeApi) {
|
getSubscribersCount(api: KubeApi): number {
|
||||||
return this.subscribers.get(api) || 0;
|
return this.subscribers.get(api) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(...apis: KubeApi[]) {
|
subscribe(...apis: KubeApi[]): () => void {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
|
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
|
||||||
});
|
});
|
||||||
return () => apis.forEach(api => {
|
return (): void => apis.forEach(api => {
|
||||||
const count = this.getSubscribersCount(api) - 1;
|
const count = this.getSubscribersCount(api) - 1;
|
||||||
if (count <= 0) this.subscribers.delete(api);
|
if (count <= 0) {
|
||||||
else this.subscribers.set(api, count);
|
this.subscribers.delete(api);
|
||||||
|
} else {
|
||||||
|
this.subscribers.set(api, count);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,16 +58,20 @@ export class KubeWatchApi {
|
|||||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||||
return {
|
return {
|
||||||
api: this.activeApis.map(api => {
|
api: this.activeApis.map(api => {
|
||||||
if (isClusterAdmin) return api.getWatchUrl();
|
if (isClusterAdmin) {
|
||||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
|
return api.getWatchUrl();
|
||||||
|
}
|
||||||
|
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
|
||||||
}).flat()
|
}).flat()
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: maybe switch to websocket to avoid often reconnects
|
// todo: maybe switch to websocket to avoid often reconnects
|
||||||
@autobind()
|
@autobind()
|
||||||
protected connect() {
|
protected connect(): void {
|
||||||
if (this.evtSource) this.disconnect(); // close previous connection
|
if (this.evtSource) {
|
||||||
|
this.disconnect();
|
||||||
|
} // close previous connection
|
||||||
if (!this.activeApis.length) {
|
if (!this.activeApis.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -76,32 +83,35 @@ export class KubeWatchApi {
|
|||||||
this.writeLog("CONNECTING", query.api);
|
this.writeLog("CONNECTING", query.api);
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect() {
|
reconnect(): void {
|
||||||
if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
|
if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
|
||||||
this.reconnectAttempts = this.maxReconnectsOnError;
|
this.reconnectAttempts = this.maxReconnectsOnError;
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected disconnect() {
|
protected disconnect(): void {
|
||||||
if (!this.evtSource) return;
|
if (!this.evtSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.evtSource.close();
|
this.evtSource.close();
|
||||||
this.evtSource.onmessage = null;
|
this.evtSource.onmessage = null;
|
||||||
this.evtSource = null;
|
this.evtSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onMessage(evt: MessageEvent) {
|
protected onMessage(evt: MessageEvent): void {
|
||||||
if (!evt.data) return;
|
if (!evt.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = JSON.parse(evt.data);
|
const data = JSON.parse(evt.data);
|
||||||
if ((data as IKubeWatchEvent).object) {
|
if ((data as IKubeWatchEvent).object) {
|
||||||
this.onData.emit(data);
|
this.onData.emit(data);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.onRouteEvent(data);
|
this.onRouteEvent(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent) {
|
protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent): Promise<void> {
|
||||||
if (type === "STREAM_END") {
|
if (type === "STREAM_END") {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
const { apiBase, namespace } = KubeApi.parseApi(url);
|
const { apiBase, namespace } = KubeApi.parseApi(url);
|
||||||
@ -111,13 +121,13 @@ export class KubeWatchApi {
|
|||||||
await api.refreshResourceVersion({ namespace });
|
await api.refreshResourceVersion({ namespace });
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.debug("failed to refresh resource version", error)
|
console.debug("failed to refresh resource version", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onError(evt: MessageEvent) {
|
protected onError(evt: MessageEvent): void {
|
||||||
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
|
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
|
||||||
if (evt.eventPhase === EventSource.CLOSED) {
|
if (evt.eventPhase === EventSource.CLOSED) {
|
||||||
if (attemptsRemain > 0) {
|
if (attemptsRemain > 0) {
|
||||||
@ -127,14 +137,14 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeLog(...data: any[]) {
|
protected writeLog(...data: any[]): void {
|
||||||
if (configStore.isDevelopment) {
|
if (configStore.isDevelopment) {
|
||||||
console.log('%cKUBE-WATCH-API:', `font-weight: bold`, ...data);
|
console.log('%cKUBE-WATCH-API:', `font-weight: bold`, ...data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
|
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void): () => void {
|
||||||
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
|
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>): void => {
|
||||||
const { selfLink, namespace, resourceVersion } = evt.object.metadata;
|
const { selfLink, namespace, resourceVersion } = evt.object.metadata;
|
||||||
const api = apiManager.getApi(selfLink);
|
const api = apiManager.getApi(selfLink);
|
||||||
api.setResourceVersion(namespace, resourceVersion);
|
api.setResourceVersion(namespace, resourceVersion);
|
||||||
@ -144,10 +154,10 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.onData.addListener(listener);
|
this.onData.addListener(listener);
|
||||||
return () => this.onData.removeListener(listener);
|
return (): void => this.onData.removeListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset(): void {
|
||||||
this.subscribers.clear();
|
this.subscribers.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { configStore } from "../config.store";
|
import { configStore } from "../config.store";
|
||||||
import { isArray } from "util";
|
import { isArray } from "util";
|
||||||
|
|
||||||
export function isAllowedResource(resources: string|string[]) {
|
export function isAllowedResource(resources: string|string[]): boolean {
|
||||||
if (!isArray(resources)) {
|
if (!isArray(resources)) {
|
||||||
resources = [resources];
|
resources = [resources];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { stringify } from "querystring";
|
|||||||
import { autobind, base64, EventEmitter, interval } from "../utils";
|
import { autobind, base64, EventEmitter, interval } from "../utils";
|
||||||
import { WebSocketApi } from "./websocket-api";
|
import { WebSocketApi } from "./websocket-api";
|
||||||
import { configStore } from "../config.store";
|
import { configStore } from "../config.store";
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
export enum TerminalChannels {
|
export enum TerminalChannels {
|
||||||
STDIN = 0,
|
STDIN = 0,
|
||||||
@ -24,7 +24,7 @@ enum TerminalColor {
|
|||||||
NO_COLOR = "\u001b[0m",
|
NO_COLOR = "\u001b[0m",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITerminalApiOptions {
|
export interface TerminalApiOptions {
|
||||||
id: string;
|
id: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
colorTheme?: "light" | "dark";
|
colorTheme?: "light" | "dark";
|
||||||
@ -38,7 +38,7 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
public onReady = new EventEmitter<[]>();
|
public onReady = new EventEmitter<[]>();
|
||||||
public isReady = false;
|
public isReady = false;
|
||||||
|
|
||||||
constructor(protected options: ITerminalApiOptions) {
|
constructor(protected options: TerminalApiOptions) {
|
||||||
super({
|
super({
|
||||||
logging: configStore.isDevelopment,
|
logging: configStore.isDevelopment,
|
||||||
flushOnOpen: false,
|
flushOnOpen: false,
|
||||||
@ -46,7 +46,7 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUrl(token: string) {
|
getUrl(token: string): string {
|
||||||
const { hostname, protocol } = location;
|
const { hostname, protocol } = location;
|
||||||
const { id, node } = this.options;
|
const { id, node } = this.options;
|
||||||
const apiPrefix = configStore.apiPrefix.TERMINAL;
|
const apiPrefix = configStore.apiPrefix.TERMINAL;
|
||||||
@ -62,9 +62,9 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
return `${wss}${hostname}${configStore.serverPort}${apiPrefix}/api?${stringify(queryParams)}`;
|
return `${wss}${hostname}${configStore.serverPort}${apiPrefix}/api?${stringify(queryParams)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect(): Promise<void> {
|
||||||
const token = await configStore.getToken();
|
const token = await configStore.getToken();
|
||||||
const apiUrl = await this.getUrl(token);
|
const apiUrl = this.getUrl(token);
|
||||||
const { colorTheme } = this.options;
|
const { colorTheme } = this.options;
|
||||||
this.emitStatus("Connecting ...", {
|
this.emitStatus("Connecting ...", {
|
||||||
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
||||||
@ -76,29 +76,35 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
async sendNewToken() {
|
async sendNewToken(): Promise<void> {
|
||||||
const token = await configStore.getToken();
|
const token = await configStore.getToken();
|
||||||
if (!this.isReady || token == this.currentToken) return;
|
if (!this.isReady || token == this.currentToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.sendCommand(token, TerminalChannels.TOKEN);
|
this.sendCommand(token, TerminalChannels.TOKEN);
|
||||||
this.currentToken = token;
|
this.currentToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(): void {
|
||||||
if (!this.socket) return;
|
if (!this.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const exitCode = String.fromCharCode(4); // ctrl+d
|
const exitCode = String.fromCharCode(4); // ctrl+d
|
||||||
this.sendCommand(exitCode);
|
this.sendCommand(exitCode);
|
||||||
this.tokenInterval.stop();
|
this.tokenInterval.stop();
|
||||||
setTimeout(() => super.destroy(), 2000);
|
setTimeout(() => super.destroy(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllListeners() {
|
removeAllListeners(): void {
|
||||||
super.removeAllListeners();
|
super.removeAllListeners();
|
||||||
this.onReady.removeAllListeners();
|
this.onReady.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
protected _onReady(data: string) {
|
protected _onReady(data: string): boolean | undefined {
|
||||||
if (!data) return;
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
this.onReady.emit();
|
this.onReady.emit();
|
||||||
this.onData.removeListener(this._onReady);
|
this.onData.removeListener(this._onReady);
|
||||||
@ -107,16 +113,15 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
return false; // prevent calling rest of listeners
|
return false; // prevent calling rest of listeners
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect() {
|
reconnect(): void {
|
||||||
const { reconnectDelaySeconds } = this.params;
|
|
||||||
super.reconnect();
|
super.reconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendCommand(key: string, channel = TerminalChannels.STDIN) {
|
sendCommand(key: string, channel = TerminalChannels.STDIN): any {
|
||||||
return this.send(channel + base64.encode(key));
|
return this.send(channel + base64.encode(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTerminalSize(cols: number, rows: number) {
|
sendTerminalSize(cols: number, rows: number): void {
|
||||||
const newSize = { Width: cols, Height: rows };
|
const newSize = { Width: cols, Height: rows };
|
||||||
if (!isEqual(this.size, newSize)) {
|
if (!isEqual(this.size, newSize)) {
|
||||||
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
|
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
|
||||||
@ -124,24 +129,24 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseMessage(data: string) {
|
protected parseMessage(data: string): any {
|
||||||
data = data.substr(1); // skip channel
|
data = data.substr(1); // skip channel
|
||||||
return base64.decode(data);
|
return base64.decode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onOpen(evt: Event) {
|
protected _onOpen(evt: Event): void {
|
||||||
// Client should send terminal size in special channel 4,
|
// Client should send terminal size in special channel 4,
|
||||||
// But this size will be changed by terminal.fit()
|
// But this size will be changed by terminal.fit()
|
||||||
this.sendTerminalSize(120, 80);
|
this.sendTerminalSize(120, 80);
|
||||||
super._onOpen(evt);
|
super._onOpen(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onClose(evt: CloseEvent) {
|
protected _onClose(evt: CloseEvent): void {
|
||||||
super._onClose(evt);
|
super._onClose(evt);
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) {
|
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}): void {
|
||||||
const { color, showTime } = options;
|
const { color, showTime } = options;
|
||||||
if (color) {
|
if (color) {
|
||||||
data = `${color}${data}${TerminalColor.NO_COLOR}`;
|
data = `${color}${data}${TerminalColor.NO_COLOR}`;
|
||||||
@ -153,7 +158,7 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
this.onData.emit(`${showTime ? time : ""}${data}\r\n`);
|
this.onData.emit(`${showTime ? time : ""}${data}\r\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected emitError(error: string) {
|
protected emitError(error: string): void {
|
||||||
this.emitStatus(error, {
|
this.emitStatus(error, {
|
||||||
color: TerminalColor.RED
|
color: TerminalColor.RED
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { EventEmitter } from "../utils/eventEmitter";
|
import { EventEmitter } from "../utils/eventEmitter";
|
||||||
|
|
||||||
interface IParams {
|
interface Params {
|
||||||
url?: string; // connection url, starts with ws:// or wss://
|
url?: string; // connection url, starts with ws:// or wss://
|
||||||
autoConnect?: boolean; // auto-connect in constructor
|
autoConnect?: boolean; // auto-connect in constructor
|
||||||
flushOnOpen?: boolean; // flush pending commands on open socket
|
flushOnOpen?: boolean; // flush pending commands on open socket
|
||||||
@ -10,7 +10,7 @@ interface IParams {
|
|||||||
logging?: boolean; // show logs in console
|
logging?: boolean; // show logs in console
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMessage {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ export enum WebSocketApiState {
|
|||||||
|
|
||||||
export class WebSocketApi {
|
export class WebSocketApi {
|
||||||
protected socket: WebSocket;
|
protected socket: WebSocket;
|
||||||
protected pendingCommands: IMessage[] = [];
|
protected pendingCommands: Message[] = [];
|
||||||
protected reconnectTimer: any;
|
protected reconnectTimer: any;
|
||||||
protected pingTimer: any;
|
protected pingTimer: any;
|
||||||
protected pingMessage = "PING";
|
protected pingMessage = "PING";
|
||||||
@ -36,7 +36,7 @@ export class WebSocketApi {
|
|||||||
public onData = new EventEmitter<[string]>();
|
public onData = new EventEmitter<[string]>();
|
||||||
public onClose = new EventEmitter<[]>();
|
public onClose = new EventEmitter<[]>();
|
||||||
|
|
||||||
static defaultParams: Partial<IParams> = {
|
static defaultParams: Partial<Params> = {
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
reconnectDelaySeconds: 10,
|
reconnectDelaySeconds: 10,
|
||||||
@ -44,7 +44,7 @@ export class WebSocketApi {
|
|||||||
flushOnOpen: true,
|
flushOnOpen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(protected params: IParams) {
|
constructor(protected params: Params) {
|
||||||
this.params = Object.assign({}, WebSocketApi.defaultParams, params);
|
this.params = Object.assign({}, WebSocketApi.defaultParams, params);
|
||||||
const { autoConnect, pingIntervalSeconds } = this.params;
|
const { autoConnect, pingIntervalSeconds } = this.params;
|
||||||
if (autoConnect) {
|
if (autoConnect) {
|
||||||
@ -55,20 +55,20 @@ export class WebSocketApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConnected() {
|
get isConnected(): boolean {
|
||||||
const state = this.socket ? this.socket.readyState : -1;
|
const state = this.socket ? this.socket.readyState : -1;
|
||||||
return state === WebSocket.OPEN && this.isOnline;
|
return state === WebSocket.OPEN && this.isOnline;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOnline() {
|
get isOnline(): boolean {
|
||||||
return navigator.onLine;
|
return navigator.onLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
setParams(params: Partial<IParams>) {
|
setParams(params: Partial<Params>): void {
|
||||||
Object.assign(this.params, params);
|
Object.assign(this.params, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(url = this.params.url) {
|
connect(url = this.params.url): void {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.close(); // close previous connection first
|
this.socket.close(); // close previous connection first
|
||||||
}
|
}
|
||||||
@ -80,21 +80,27 @@ export class WebSocketApi {
|
|||||||
this.readyState = WebSocketApiState.CONNECTING;
|
this.readyState = WebSocketApiState.CONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
ping() {
|
ping(): void {
|
||||||
if (!this.isConnected) return;
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.send(this.pingMessage);
|
this.send(this.pingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect() {
|
reconnect(): void {
|
||||||
const { reconnectDelaySeconds } = this.params;
|
const { reconnectDelaySeconds } = this.params;
|
||||||
if (!reconnectDelaySeconds) return;
|
if (!reconnectDelaySeconds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.writeLog('reconnect after', reconnectDelaySeconds + "ms");
|
this.writeLog('reconnect after', reconnectDelaySeconds + "ms");
|
||||||
this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000);
|
this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000);
|
||||||
this.readyState = WebSocketApiState.RECONNECTING;
|
this.readyState = WebSocketApiState.RECONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(): void {
|
||||||
if (!this.socket) return;
|
if (!this.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.pendingCommands = [];
|
this.pendingCommands = [];
|
||||||
@ -104,64 +110,60 @@ export class WebSocketApi {
|
|||||||
this.readyState = WebSocketApiState.PENDING;
|
this.readyState = WebSocketApiState.PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllListeners() {
|
removeAllListeners(): void {
|
||||||
this.onOpen.removeAllListeners();
|
this.onOpen.removeAllListeners();
|
||||||
this.onData.removeAllListeners();
|
this.onData.removeAllListeners();
|
||||||
this.onClose.removeAllListeners();
|
this.onClose.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
send(command: string) {
|
send(command: string): void {
|
||||||
const msg: IMessage = {
|
const msg: Message = {
|
||||||
id: (Math.random() * Date.now()).toString(16).replace(".", ""),
|
id: (Math.random() * Date.now()).toString(16).replace(".", ""),
|
||||||
data: command,
|
data: command,
|
||||||
};
|
};
|
||||||
if (this.isConnected) {
|
if (this.isConnected) {
|
||||||
this.socket.send(msg.data);
|
this.socket.send(msg.data);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.pendingCommands.push(msg);
|
this.pendingCommands.push(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected flush() {
|
protected flush(): void {
|
||||||
this.pendingCommands.forEach(msg => this.send(msg.data));
|
this.pendingCommands.forEach(msg => this.send(msg.data));
|
||||||
this.pendingCommands.length = 0;
|
this.pendingCommands.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseMessage(data: string) {
|
protected _onOpen(evt: Event): void {
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _onOpen(evt: Event) {
|
|
||||||
this.onOpen.emit();
|
this.onOpen.emit();
|
||||||
if (this.params.flushOnOpen) this.flush();
|
if (this.params.flushOnOpen) {
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
this.readyState = WebSocketApiState.OPEN;
|
this.readyState = WebSocketApiState.OPEN;
|
||||||
this.writeLog('%cOPEN', 'color:green;font-weight:bold;', evt);
|
this.writeLog('%cOPEN', 'color:green;font-weight:bold;', evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onMessage(evt: MessageEvent) {
|
protected _onMessage(evt: MessageEvent): void {
|
||||||
const data = this.parseMessage(evt.data);
|
const data = evt.data;
|
||||||
this.onData.emit(data);
|
this.onData.emit(data);
|
||||||
this.writeLog('%cMESSAGE', 'color:black;font-weight:bold;', data);
|
this.writeLog('%cMESSAGE', 'color:black;font-weight:bold;', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onError(evt: Event) {
|
protected _onError(evt: Event): void {
|
||||||
this.writeLog('%cERROR', 'color:red;font-weight:bold;', evt)
|
this.writeLog('%cERROR', 'color:red;font-weight:bold;', evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onClose(evt: CloseEvent) {
|
protected _onClose(evt: CloseEvent): void {
|
||||||
const error = evt.code !== 1000 || !evt.wasClean;
|
const error = evt.code !== 1000 || !evt.wasClean;
|
||||||
if (error) {
|
if (error) {
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.readyState = WebSocketApiState.CLOSED;
|
this.readyState = WebSocketApiState.CLOSED;
|
||||||
this.onClose.emit();
|
this.onClose.emit();
|
||||||
}
|
}
|
||||||
this.writeLog('%cCLOSE', `color:${error ? "red" : "black"};font-weight:bold;`, evt);
|
this.writeLog('%cCLOSE', `color:${error ? "red" : "black"};font-weight:bold;`, evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected writeLog(...data: any[]) {
|
protected writeLog(...data: any[]): void {
|
||||||
if (this.params.logging) {
|
if (this.params.logging) {
|
||||||
console.log(...data);
|
console.log(...data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,48 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { IKubeObjectMetadata, KubeObject } from "./kube-object";
|
import { KubeObject } from "./kube-object";
|
||||||
|
|
||||||
interface IToleration {
|
interface Toleration {
|
||||||
key?: string;
|
key?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
effect?: string;
|
effect?: string;
|
||||||
tolerationSeconds?: number;
|
tolerationSeconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMatchExpression {
|
interface MatchExpression {
|
||||||
key: string;
|
key: string;
|
||||||
operator: string;
|
operator: string;
|
||||||
values: string[];
|
values: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INodeAffinity {
|
interface NodeAffinity {
|
||||||
nodeSelectorTerms?: {
|
nodeSelectorTerms?: {
|
||||||
matchExpressions: IMatchExpression[];
|
matchExpressions: MatchExpression[];
|
||||||
}[];
|
}[];
|
||||||
weight: number;
|
weight: number;
|
||||||
preference: {
|
preference: {
|
||||||
matchExpressions: IMatchExpression[];
|
matchExpressions: MatchExpression[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPodAffinity {
|
interface PodAffinity {
|
||||||
labelSelector: {
|
labelSelector: {
|
||||||
matchExpressions: IMatchExpression[];
|
matchExpressions: MatchExpression[];
|
||||||
};
|
};
|
||||||
topologyKey: string;
|
topologyKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAffinity {
|
export interface Affinity {
|
||||||
nodeAffinity?: {
|
nodeAffinity?: {
|
||||||
requiredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[];
|
requiredDuringSchedulingIgnoredDuringExecution?: NodeAffinity[];
|
||||||
preferredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[];
|
preferredDuringSchedulingIgnoredDuringExecution?: NodeAffinity[];
|
||||||
};
|
};
|
||||||
podAffinity?: {
|
podAffinity?: {
|
||||||
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
requiredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
|
||||||
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
preferredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
|
||||||
};
|
};
|
||||||
podAntiAffinity?: {
|
podAntiAffinity?: {
|
||||||
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
requiredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
|
||||||
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
preferredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,17 +66,19 @@ export class WorkloadKubeObject extends KubeObject {
|
|||||||
return KubeObject.stringifyLabels(labels);
|
return KubeObject.stringifyLabels(labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTolerations(): IToleration[] {
|
getTolerations(): Toleration[] {
|
||||||
return get(this, "spec.template.spec.tolerations", [])
|
return get(this, "spec.template.spec.tolerations", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAffinity(): IAffinity {
|
getAffinity(): Affinity {
|
||||||
return get(this, "spec.template.spec.affinity")
|
return get(this, "spec.template.spec.affinity");
|
||||||
}
|
}
|
||||||
|
|
||||||
getAffinityNumber() {
|
getAffinityNumber(): number {
|
||||||
const affinity = this.getAffinity()
|
const affinity = this.getAffinity();
|
||||||
if (!affinity) return 0
|
if (!affinity) {
|
||||||
return Object.keys(affinity).length
|
return 0;
|
||||||
|
}
|
||||||
|
return Object.keys(affinity).length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,11 +2,11 @@ import * as React from "react";
|
|||||||
import { Notifications } from "./components/notifications";
|
import { Notifications } from "./components/notifications";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
|
|
||||||
export function browserCheck() {
|
export function browserCheck(): void {
|
||||||
const ua = window.navigator.userAgent
|
const ua = window.navigator.userAgent;
|
||||||
const msie = ua.indexOf('MSIE ') // IE < 11
|
const msie = ua.indexOf('MSIE '); // IE < 11
|
||||||
const trident = ua.indexOf('Trident/') // IE 11
|
const trident = ua.indexOf('Trident/'); // IE 11
|
||||||
const edge = ua.indexOf('Edge') // Edge
|
const edge = ua.indexOf('Edge'); // Edge
|
||||||
if (msie > 0 || trident > 0 || edge > 0) {
|
if (msie > 0 || trident > 0 || edge > 0) {
|
||||||
Notifications.info(
|
Notifications.info(
|
||||||
<p>
|
<p>
|
||||||
@ -15,6 +15,6 @@ export function browserCheck() {
|
|||||||
Please consider using another browser.
|
Please consider using another browser.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./not-found"
|
export * from "./not-found";
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
|
|
||||||
export class NotFound extends React.Component {
|
export class NotFound extends React.Component {
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MainLayout className="NotFound" contentClass="flex" footer={null}>
|
<MainLayout className="NotFound" contentClass="flex" footer={null}>
|
||||||
<p className="box center">
|
<p className="box center">
|
||||||
<Trans>Page not found</Trans>
|
<Trans>Page not found</Trans>
|
||||||
</p>
|
</p>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,6 +16,9 @@ import { createInstallChartTab } from "../dock/install-chart.store";
|
|||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const placeholder = require("./helm-placeholder.svg");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chart: HelmChart;
|
chart: HelmChart;
|
||||||
hideDetails(): void;
|
hideDetails(): void;
|
||||||
@ -31,7 +34,9 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
chartSelector = autorun(async () => {
|
chartSelector = autorun(async () => {
|
||||||
if (!this.props.chart) return;
|
if (!this.props.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.chartVersions = null;
|
this.chartVersions = null;
|
||||||
this.selectedChart = null;
|
this.selectedChart = null;
|
||||||
this.description = null;
|
this.description = null;
|
||||||
@ -43,42 +48,43 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
loadChartData(version?: string) {
|
loadChartData(version?: string): void {
|
||||||
const { chart: { name, repo } } = this.props;
|
const { chart: { name, repo } } = this.props;
|
||||||
if (this.chartPromise) this.chartPromise.cancel();
|
if (this.chartPromise) {
|
||||||
|
this.chartPromise.cancel();
|
||||||
|
}
|
||||||
this.chartPromise = helmChartsApi.get(repo, name, version);
|
this.chartPromise = helmChartsApi.get(repo, name, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onVersionChange(opt: SelectOption) {
|
onVersionChange(opt: SelectOption): void {
|
||||||
const version = opt.value;
|
const version = opt.value;
|
||||||
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
||||||
this.description = null;
|
this.description = null;
|
||||||
this.loadChartData(version);
|
this.loadChartData(version);
|
||||||
this.chartPromise.then(data => {
|
this.chartPromise.then(data => {
|
||||||
this.description = data.readme
|
this.description = data.readme;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
install() {
|
install(): void {
|
||||||
createInstallChartTab(this.selectedChart);
|
createInstallChartTab(this.selectedChart);
|
||||||
this.props.hideDetails()
|
this.props.hideDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIntroduction() {
|
renderIntroduction(): JSX.Element {
|
||||||
const { selectedChart, chartVersions, onVersionChange } = this;
|
const { selectedChart, chartVersions, onVersionChange } = this;
|
||||||
const placeholder = require("./helm-placeholder.svg");
|
|
||||||
return (
|
return (
|
||||||
<div className="introduction flex align-flex-start">
|
<div className="introduction flex align-flex-start">
|
||||||
<img
|
<img
|
||||||
className="intro-logo"
|
className="intro-logo"
|
||||||
src={selectedChart.getIcon() || placeholder}
|
src={selectedChart.icon || placeholder}
|
||||||
onError={(event) => event.currentTarget.src = placeholder}
|
onError={(event: React.SyntheticEvent<HTMLImageElement, Event>): void => event.currentTarget.src = placeholder}
|
||||||
/>
|
/>
|
||||||
<div className="intro-contents box grow">
|
<div className="intro-contents box grow">
|
||||||
<div className="description flex align-center justify-space-between">
|
<div className="description flex align-center justify-space-between">
|
||||||
{selectedChart.getDescription()}
|
{selectedChart.description}
|
||||||
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
|
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
|
||||||
</div>
|
</div>
|
||||||
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
|
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
|
||||||
@ -86,16 +92,16 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
themeName="outlined"
|
themeName="outlined"
|
||||||
menuPortalTarget={null}
|
menuPortalTarget={null}
|
||||||
options={chartVersions.map(chart => chart.version)}
|
options={chartVersions.map(chart => chart.version)}
|
||||||
value={selectedChart.getVersion()}
|
value={selectedChart.version}
|
||||||
onChange={onVersionChange}
|
onChange={onVersionChange}
|
||||||
/>
|
/>
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
<DrawerItem name={_i18n._(t`Home`)}>
|
<DrawerItem name={_i18n._(t`Home`)}>
|
||||||
<a href={selectedChart.getHome()} target="_blank">{selectedChart.getHome()}</a>
|
<a href={selectedChart.home} target="_blank" rel="noreferrer">{selectedChart.home}</a>
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
|
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
|
||||||
{selectedChart.getMaintainers().map(({ name, email, url }) =>
|
{selectedChart.getMaintainers().map(({ name, email, url }) =>
|
||||||
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a>
|
<a key={name} href={url ? url : `mailto:${email}`} target="_blank" rel="noreferrer">{name}</a>
|
||||||
)}
|
)}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
{selectedChart.getKeywords().length > 0 && (
|
{selectedChart.getKeywords().length > 0 && (
|
||||||
@ -108,8 +114,10 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent(): JSX.Element {
|
||||||
if (this.selectedChart === null || this.description === null) return <Spinner center/>;
|
if (this.selectedChart === null || this.description === null) {
|
||||||
|
return <Spinner center/>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="box grow">
|
<div className="box grow">
|
||||||
{this.renderIntroduction()}
|
{this.renderIntroduction()}
|
||||||
@ -120,7 +128,7 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { chart, hideDetails } = this.props;
|
const { chart, hideDetails } = this.props;
|
||||||
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
|
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,51 +2,48 @@ import { observable } from "mobx";
|
|||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||||
import { ItemStore } from "../../item.store";
|
import { ItemStore } from "../../item.store";
|
||||||
import flatten from "lodash/flatten"
|
import flatten from "lodash/flatten";
|
||||||
import compareVersions from 'compare-versions';
|
import compareVersions from 'compare-versions';
|
||||||
|
|
||||||
export interface IChartVersion {
|
export interface ChartVersion {
|
||||||
repo: string;
|
repo: string;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class HelmChartStore extends ItemStore<HelmChart> {
|
export class HelmChartStore extends ItemStore<HelmChart> {
|
||||||
@observable versions = observable.map<string, IChartVersion[]>();
|
@observable versions = observable.map<string, ChartVersion[]>();
|
||||||
|
|
||||||
loadAll() {
|
async loadAll(): Promise<void> {
|
||||||
return this.loadItems(() => helmChartsApi.list());
|
await this.loadItems(() => helmChartsApi.list());
|
||||||
}
|
}
|
||||||
|
|
||||||
getByName(name: string, repo: string) {
|
getByName(desiredName: string, desiredRepo: string): HelmChart {
|
||||||
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo);
|
return this.items.find(({ name, repo }) => desiredName === name && desiredRepo === repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected sortVersions = (versions: IChartVersion[]) => {
|
protected sortVersions = (versions: ChartVersion[]): ChartVersion[] => {
|
||||||
return versions.sort((first, second) => {
|
return versions.sort((first, second) => compareVersions(second.version, first.version));
|
||||||
return compareVersions(second.version, first.version)
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
|
async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
|
||||||
let versions = this.versions.get(chartName);
|
let versions = this.versions.get(chartName);
|
||||||
if (versions && !force) {
|
if (versions && !force) {
|
||||||
return versions;
|
return versions;
|
||||||
}
|
}
|
||||||
const loadVersions = (repo: string) => {
|
const loadVersions = async (repo: string): Promise<ChartVersion[]> => {
|
||||||
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
|
const { versions } = await helmChartsApi.get(repo, chartName);
|
||||||
return versions.map(chart => ({
|
return versions.map(({version}) => ({ repo, version, }));
|
||||||
repo: repo,
|
|
||||||
version: chart.getVersion()
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.isLoaded) {
|
if (!this.isLoaded) {
|
||||||
await this.loadAll();
|
await this.loadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
const repos = this.items
|
const repos = this.items
|
||||||
.filter(chart => chart.getName() === chartName)
|
.filter(chart => chart.getName() === chartName)
|
||||||
.map(chart => chart.getRepository());
|
.map(({repo}) => repo);
|
||||||
|
|
||||||
versions = await Promise.all(repos.map(loadVersions))
|
versions = await Promise.all(repos.map(loadVersions))
|
||||||
.then(flatten)
|
.then(flatten)
|
||||||
.then(this.sortVersions);
|
.then(this.sortVersions);
|
||||||
@ -55,7 +52,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
|||||||
return versions;
|
return versions;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset(): void {
|
||||||
super.reset();
|
super.reset();
|
||||||
this.versions.clear();
|
this.versions.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { RouteProps } from "react-router"
|
import { RouteProps } from "react-router";
|
||||||
import { appsRoute } from "../+apps/apps.route";
|
import { appsRoute } from "../+apps/apps.route";
|
||||||
import { buildURL } from "../../navigation";
|
import { buildURL } from "../../navigation";
|
||||||
|
|
||||||
export const helmChartsRoute: RouteProps = {
|
export const helmChartsRoute: RouteProps = {
|
||||||
path: appsRoute.path + "/charts/:repo?/:chartName?"
|
path: appsRoute.path + "/charts/:repo?/:chartName?"
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface IHelmChartsRouteParams {
|
export interface HelmChartsRouteParams {
|
||||||
chartName?: string;
|
chartName?: string;
|
||||||
repo?: string;
|
repo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helmChartsURL = buildURL<IHelmChartsRouteParams>(helmChartsRoute.path)
|
export const helmChartsURL = buildURL<HelmChartsRouteParams>(helmChartsRoute.path);
|
||||||
@ -3,7 +3,7 @@ import "./helm-charts.scss";
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { helmChartsURL, IHelmChartsRouteParams } from "./helm-charts.route";
|
import { helmChartsURL, HelmChartsRouteParams } from "./helm-charts.route";
|
||||||
import { helmChartStore } from "./helm-chart.store";
|
import { helmChartStore } from "./helm-chart.store";
|
||||||
import { HelmChart } from "../../api/endpoints/helm-charts.api";
|
import { HelmChart } from "../../api/endpoints/helm-charts.api";
|
||||||
import { HelmChartDetails } from "./helm-chart-details";
|
import { HelmChartDetails } from "./helm-chart-details";
|
||||||
@ -18,39 +18,38 @@ enum sortBy {
|
|||||||
repo = "repo",
|
repo = "repo",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IHelmChartsRouteParams> {
|
interface Props extends RouteComponentProps<HelmChartsRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HelmCharts extends Component<Props> {
|
export class HelmCharts extends Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
helmChartStore.loadAll();
|
helmChartStore.loadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedChart() {
|
get selectedChart(): HelmChart {
|
||||||
const { match: { params: { chartName, repo } } } = this.props
|
const { match: { params: { chartName, repo } } } = this.props;
|
||||||
return helmChartStore.getByName(chartName, repo);
|
return helmChartStore.getByName(chartName, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetails = (chart: HelmChart) => {
|
showDetails = (chart: HelmChart): void => {
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
navigation.merge(helmChartsURL())
|
navigation.merge(helmChartsURL());
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
navigation.merge(helmChartsURL({
|
navigation.merge(helmChartsURL({
|
||||||
params: {
|
params: {
|
||||||
chartName: chart.getName(),
|
chartName: chart.getName(),
|
||||||
repo: chart.getRepository(),
|
repo: chart.repo,
|
||||||
}
|
}
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideDetails = () => {
|
hideDetails = (): void => {
|
||||||
this.showDetails(null);
|
this.showDetails(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
@ -59,19 +58,19 @@ export class HelmCharts extends Component<Props> {
|
|||||||
isClusterScoped={true}
|
isClusterScoped={true}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (chart: HelmChart) => chart.getName(),
|
[sortBy.name]: (chart: HelmChart): string => chart.getName(),
|
||||||
[sortBy.repo]: (chart: HelmChart) => chart.getRepository(),
|
[sortBy.repo]: ({repo}: HelmChart): string => repo,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(chart: HelmChart) => chart.getName(),
|
(chart: HelmChart): string => chart.getName(),
|
||||||
(chart: HelmChart) => chart.getVersion(),
|
({ version }: HelmChart): string => version,
|
||||||
(chart: HelmChart) => chart.getAppVersion(),
|
(chart: HelmChart): string => chart.getAppVersion(),
|
||||||
(chart: HelmChart) => chart.getKeywords(),
|
({ keywords }: HelmChart): string[] => keywords,
|
||||||
]}
|
]}
|
||||||
filterItems={[
|
filterItems={[
|
||||||
(items: HelmChart[]) => items.filter(item => !item.deprecated)
|
(items: HelmChart[]): HelmChart[] => items.filter(item => !item.deprecated)
|
||||||
]}
|
]}
|
||||||
customizeHeader={() => (
|
customizeHeader={(): JSX.Element => (
|
||||||
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
|
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
|
||||||
)}
|
)}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -83,18 +82,18 @@ export class HelmCharts extends Component<Props> {
|
|||||||
{ title: <Trans>Repository</Trans>, className: "repository", sortBy: sortBy.repo },
|
{ title: <Trans>Repository</Trans>, className: "repository", sortBy: sortBy.repo },
|
||||||
|
|
||||||
]}
|
]}
|
||||||
renderTableContents={(chart: HelmChart) => [
|
renderTableContents={(chart: HelmChart): (HTMLElement | string | React.ReactNode)[] => [
|
||||||
<figure>
|
<figure key="placeholder-img">
|
||||||
<img
|
<img
|
||||||
src={chart.getIcon() || require("./helm-placeholder.svg")}
|
src={chart.icon || require("./helm-placeholder.svg")}
|
||||||
onLoad={evt => evt.currentTarget.classList.add("visible")}
|
onLoad={(evt): void => evt.currentTarget.classList.add("visible")}
|
||||||
/>
|
/>
|
||||||
</figure>,
|
</figure>,
|
||||||
chart.getName(),
|
chart.getName(),
|
||||||
chart.getDescription(),
|
chart.description,
|
||||||
chart.getVersion(),
|
chart.version,
|
||||||
chart.getAppVersion(),
|
chart.getAppVersion(),
|
||||||
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() }
|
{ title: chart.repo, className: chart.repo.toLowerCase() }
|
||||||
]}
|
]}
|
||||||
detailsItem={this.selectedChart}
|
detailsItem={this.selectedChart}
|
||||||
onDetails={this.showDetails}
|
onDetails={this.showDetails}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { observable, reaction } from "mobx";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
|
import { HelmRelease, helmReleasesApi, ReleaseInfo } from "../../api/endpoints/helm-releases.api";
|
||||||
import { HelmReleaseMenu } from "./release-menu";
|
import { HelmReleaseMenu } from "./release-menu";
|
||||||
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
|
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
@ -35,14 +35,16 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ReleaseDetails extends Component<Props> {
|
export class ReleaseDetails extends Component<Props> {
|
||||||
@observable details: IReleaseDetails;
|
@observable details: ReleaseInfo;
|
||||||
@observable values = "";
|
@observable values = "";
|
||||||
@observable saving = false;
|
@observable saving = false;
|
||||||
@observable releaseSecret: Secret;
|
@observable releaseSecret: Secret;
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
releaseSelector = reaction(() => this.props.release, release => {
|
releaseSelector = reaction(() => this.props.release, release => {
|
||||||
if (!release) return;
|
if (!release) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.loadDetails();
|
this.loadDetails();
|
||||||
this.loadValues();
|
this.loadValues();
|
||||||
this.releaseSecret = null;
|
this.releaseSecret = null;
|
||||||
@ -51,33 +53,36 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
||||||
if (!this.props.release) return;
|
if (!this.props.release) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { getReleaseSecret } = releaseStore;
|
const { getReleaseSecret } = releaseStore;
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
const secret = getReleaseSecret(release);
|
const secret = getReleaseSecret(release);
|
||||||
if (this.releaseSecret) {
|
if (this.releaseSecret) {
|
||||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.loadDetails();
|
this.loadDetails();
|
||||||
}
|
}
|
||||||
this.releaseSecret = secret;
|
this.releaseSecret = secret;
|
||||||
});
|
});
|
||||||
|
|
||||||
async loadDetails() {
|
async loadDetails(): Promise<void> {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
this.details = null;
|
this.details = null;
|
||||||
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
|
this.details = await helmReleasesApi.get(release.getName(), release.namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadValues() {
|
async loadValues(): Promise<void> {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
this.values = "";
|
this.values = "";
|
||||||
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
|
this.values = await helmReleasesApi.getValues(release.getName(), release.namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValues = async () => {
|
updateValues = async (): Promise<void> => {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
const name = release.getName();
|
const { namespace, name} = release;
|
||||||
const namespace = release.getNs()
|
|
||||||
const data = {
|
const data = {
|
||||||
chart: release.getChart(),
|
chart: release.getChart(),
|
||||||
repo: await release.getRepo(),
|
repo: await release.getRepo(),
|
||||||
@ -96,13 +101,13 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
upgradeVersion = () => {
|
upgradeVersion = (): void => {
|
||||||
const { release, hideDetails } = this.props;
|
const { release, hideDetails } = this.props;
|
||||||
createUpgradeChartTab(release);
|
createUpgradeChartTab(release);
|
||||||
hideDetails();
|
hideDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValues() {
|
renderValues(): JSX.Element {
|
||||||
const { values, saving } = this;
|
const { values, saving } = this;
|
||||||
return (
|
return (
|
||||||
<div className="values">
|
<div className="values">
|
||||||
@ -111,7 +116,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
<AceEditor
|
<AceEditor
|
||||||
mode="yaml"
|
mode="yaml"
|
||||||
value={values}
|
value={values}
|
||||||
onChange={values => this.values = values}
|
onChange={(values): string => this.values = values}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
@ -121,11 +126,13 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNotes() {
|
renderNotes(): JSX.Element {
|
||||||
if (!this.details.info?.notes) return null;
|
if (!this.details.info?.notes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const { notes } = this.details.info;
|
const { notes } = this.details.info;
|
||||||
return (
|
return (
|
||||||
<div className="notes">
|
<div className="notes">
|
||||||
@ -134,9 +141,11 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderResources() {
|
renderResources(): JSX.Element {
|
||||||
const { resources } = this.details;
|
const { resources } = this.details;
|
||||||
if (!resources) return null;
|
if (!resources) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const groups = groupBy(resources, item => item.kind);
|
const groups = groupBy(resources, item => item.kind);
|
||||||
const tables = Object.entries(groups).map(([kind, items]) => {
|
const tables = Object.entries(groups).map(([kind, items]) => {
|
||||||
return (
|
return (
|
||||||
@ -177,10 +186,12 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent(): JSX.Element {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
const { details } = this;
|
const { details } = this;
|
||||||
if (!release) return null;
|
if (!release) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!details) {
|
if (!details) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
@ -201,7 +212,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
{release.getUpdated()} <Trans>ago</Trans> ({release.updated})
|
{release.getUpdated()} <Trans>ago</Trans> ({release.updated})
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
<DrawerItem name={<Trans>Namespace</Trans>}>
|
<DrawerItem name={<Trans>Namespace</Trans>}>
|
||||||
{release.getNs()}
|
{release.namespace}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
<DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}>
|
<DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}>
|
||||||
<div className="version flex gaps align-center">
|
<div className="version flex gaps align-center">
|
||||||
@ -222,13 +233,13 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
<DrawerTitle title={_i18n._(t`Resources`)}/>
|
<DrawerTitle title={_i18n._(t`Resources`)}/>
|
||||||
{this.renderResources()}
|
{this.renderResources()}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { release, hideDetails } = this.props
|
const { release, hideDetails } = this.props;
|
||||||
const title = release ? <Trans>Release: {release.getName()}</Trans> : ""
|
const title = release ? <Trans>Release: {release.getName()}</Trans> : "";
|
||||||
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>
|
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
||||||
@ -240,6 +251,6 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
>
|
>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,26 +17,28 @@ interface Props extends MenuActionsProps {
|
|||||||
|
|
||||||
export class HelmReleaseMenu extends React.Component<Props> {
|
export class HelmReleaseMenu extends React.Component<Props> {
|
||||||
@autobind()
|
@autobind()
|
||||||
remove() {
|
remove(): Promise<void> {
|
||||||
return releaseStore.remove(this.props.release);
|
return releaseStore.remove(this.props.release);
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
upgrade() {
|
upgrade(): void {
|
||||||
const { release, hideDetails } = this.props;
|
const { release, hideDetails } = this.props;
|
||||||
createUpgradeChartTab(release);
|
createUpgradeChartTab(release);
|
||||||
hideDetails && hideDetails();
|
hideDetails && hideDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
rollback() {
|
rollback(): void {
|
||||||
ReleaseRollbackDialog.open(this.props.release);
|
ReleaseRollbackDialog.open(this.props.release);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent(): JSX.Element {
|
||||||
const { release, toolbar } = this.props;
|
const { release, toolbar } = this.props;
|
||||||
if (!release) return;
|
if (!release) {
|
||||||
const hasRollback = release && release.getRevision() > 1;
|
return;
|
||||||
|
}
|
||||||
|
const hasRollback = release && release.revision > 1;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasRollback && (
|
{hasRollback && (
|
||||||
@ -46,18 +48,19 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { className, release, ...menuProps } = this.props;
|
const { className, release: _release, ...menuProps } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuActions
|
<MenuActions
|
||||||
{...menuProps}
|
{...menuProps}
|
||||||
className={cssNames("HelmReleaseMenu", className)}
|
className={cssNames("HelmReleaseMenu", className)}
|
||||||
removeAction={this.remove}
|
removeAction={this.remove}
|
||||||
children={this.renderContent()}
|
>
|
||||||
/>
|
{this.renderContent()}
|
||||||
|
</MenuActions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { observer } from "mobx-react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { Dialog, DialogProps } from "../dialog";
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
import { Wizard, WizardStep } from "../wizard";
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
|
import { HelmRelease, helmReleasesApi, ReleaseRevision } from "../../api/endpoints/helm-releases.api";
|
||||||
import { releaseStore } from "./release.store";
|
import { releaseStore } from "./release.store";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import orderBy from "lodash/orderBy"
|
import orderBy from "lodash/orderBy";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
}
|
}
|
||||||
@ -21,15 +21,15 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
@observable.ref static release: HelmRelease = null;
|
@observable.ref static release: HelmRelease = null;
|
||||||
|
|
||||||
@observable isLoading = false;
|
@observable isLoading = false;
|
||||||
@observable revision: IReleaseRevision;
|
@observable revision: ReleaseRevision;
|
||||||
@observable revisions = observable.array<IReleaseRevision>();
|
@observable revisions = observable.array<ReleaseRevision>();
|
||||||
|
|
||||||
static open(release: HelmRelease) {
|
static open(release: HelmRelease): void {
|
||||||
ReleaseRollbackDialog.isOpen = true;
|
ReleaseRollbackDialog.isOpen = true;
|
||||||
ReleaseRollbackDialog.release = release;
|
ReleaseRollbackDialog.release = release;
|
||||||
}
|
}
|
||||||
|
|
||||||
static close() {
|
static close(): void {
|
||||||
ReleaseRollbackDialog.isOpen = false;
|
ReleaseRollbackDialog.isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
return ReleaseRollbackDialog.release;
|
return ReleaseRollbackDialog.release;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen = async () => {
|
onOpen = async (): Promise<void> => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const currentRevision = this.release.getRevision();
|
const currentRevision = this.release.revision;
|
||||||
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
|
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.namespace);
|
||||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
||||||
releases = orderBy(releases, "revision", "desc"); // sort
|
releases = orderBy(releases, "revision", "desc"); // sort
|
||||||
this.revisions.replace(releases);
|
this.revisions.replace(releases);
|
||||||
@ -48,24 +48,24 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rollback = async () => {
|
rollback = async (): Promise<void> => {
|
||||||
const revisionNumber = this.revision.revision;
|
const revisionNumber = this.revision.revision;
|
||||||
try {
|
try {
|
||||||
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
await releaseStore.rollback(this.release.getName(), this.release.namespace, revisionNumber);
|
||||||
this.close();
|
this.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error(err);
|
Notifications.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
close = () => {
|
close = (): void => {
|
||||||
ReleaseRollbackDialog.close();
|
ReleaseRollbackDialog.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent(): JSX.Element {
|
||||||
const { revision, revisions } = this;
|
const { revision, revisions } = this;
|
||||||
if (!revision) {
|
if (!revision) {
|
||||||
return <p><Trans>No revisions to rollback.</Trans></p>
|
return <p><Trans>No revisions to rollback.</Trans></p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
@ -74,17 +74,19 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
themeName="light"
|
themeName="light"
|
||||||
value={revision}
|
value={revision}
|
||||||
options={revisions}
|
options={revisions}
|
||||||
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`}
|
formatOptionLabel={({ value }: SelectOption<ReleaseRevision>): string => `${value.revision} - ${value.chart}`}
|
||||||
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
|
onChange={({ value }: SelectOption<ReleaseRevision>): void => {
|
||||||
|
this.revision = value;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { ...dialogProps } = this.props;
|
const { ...dialogProps } = this.props;
|
||||||
const releaseName = this.release ? this.release.getName() : "";
|
const releaseName = this.release ? this.release.getName() : "";
|
||||||
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>
|
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>;
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialogProps}
|
{...dialogProps}
|
||||||
@ -104,6 +106,6 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
</WizardStep>
|
</WizardStep>
|
||||||
</Wizard>
|
</Wizard>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { RouteProps } from "react-router"
|
import { RouteProps } from "react-router";
|
||||||
import { appsRoute } from "../+apps/apps.route";
|
import { appsRoute } from "../+apps/apps.route";
|
||||||
import { buildURL } from "../../navigation";
|
import { buildURL } from "../../navigation";
|
||||||
|
|
||||||
export const releaseRoute: RouteProps = {
|
export const releaseRoute: RouteProps = {
|
||||||
path: appsRoute.path + "/releases/:namespace?/:name?"
|
path: appsRoute.path + "/releases/:namespace?/:name?"
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface IReleaseRouteParams {
|
export interface ReleaseRouteParams {
|
||||||
name?: string;
|
name?: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const releaseURL = buildURL<IReleaseRouteParams>(releaseRoute.path);
|
export const releaseURL = buildURL<ReleaseRouteParams>(releaseRoute.path);
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
|
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
import { HelmRelease, helmReleasesApi, ReleaseCreatePayload, ReleaseUpdatePayload, ReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
|
||||||
import { ItemStore } from "../../item.store";
|
import { ItemStore } from "../../item.store";
|
||||||
import { configStore } from "../../config.store";
|
import { configStore } from "../../config.store";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
import { Secret } from "../../api/endpoints";
|
import { Secret } from "../../api/endpoints";
|
||||||
|
import { KubeJsonApiData } from "client/api/kube-json-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||||
@ -19,47 +20,51 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch() {
|
watch(): void {
|
||||||
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
||||||
if (this.isLoading) return;
|
if (this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const secrets = this.getReleaseSecrets();
|
const secrets = this.getReleaseSecrets();
|
||||||
const amountChanged = secrets.length !== this.releaseSecrets.length;
|
const amountChanged = secrets.length !== this.releaseSecrets.length;
|
||||||
const labelsChanged = this.releaseSecrets.some(item => {
|
const labelsChanged = this.releaseSecrets.some(item => {
|
||||||
const secret = secrets.find(secret => secret.getId() == item.getId());
|
const secret = secrets.find(secret => secret.getId() == item.getId());
|
||||||
if (!secret) return;
|
if (!secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return !isEqual(item.getLabels(), secret.getLabels());
|
return !isEqual(item.getLabels(), secret.getLabels());
|
||||||
});
|
});
|
||||||
if (amountChanged || labelsChanged) {
|
if (amountChanged || labelsChanged) {
|
||||||
this.loadAll();
|
this.loadAll();
|
||||||
}
|
}
|
||||||
this.releaseSecrets = [...secrets];
|
this.releaseSecrets = [...secrets];
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
unwatch() {
|
unwatch(): void {
|
||||||
this.secretWatcher();
|
this.secretWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
getReleaseSecrets() {
|
getReleaseSecrets(): Secret[] {
|
||||||
return secretsStore.getByLabel({ owner: "helm" });
|
return secretsStore.getByLabel({ owner: "helm" });
|
||||||
}
|
}
|
||||||
|
|
||||||
getReleaseSecret(release: HelmRelease) {
|
getReleaseSecret(release: HelmRelease): Secret {
|
||||||
const labels = {
|
const labels = {
|
||||||
owner: "helm",
|
owner: "helm",
|
||||||
name: release.getName()
|
name: release.getName()
|
||||||
}
|
};
|
||||||
return secretsStore.getByLabel(labels)
|
return secretsStore.getByLabel(labels)
|
||||||
.filter(secret => secret.getNs() == release.getNs())[0];
|
.filter(secret => secret.getNs() == release.namespace)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll(): Promise<void> {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items;
|
let items: HelmRelease[];
|
||||||
try {
|
try {
|
||||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
items = await this.loadItems(...(!isClusterAdmin ? allowedNamespaces : []));
|
||||||
} finally {
|
} finally {
|
||||||
if (items) {
|
if (items) {
|
||||||
items = this.sortItems(items);
|
items = this.sortItems(items);
|
||||||
@ -70,42 +75,49 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadItems(namespaces?: string[]) {
|
async loadItems(...namespaces: any[]): Promise<HelmRelease[]> {
|
||||||
if (!namespaces) {
|
if (!namespaces) {
|
||||||
return helmReleasesApi.list();
|
return helmReleasesApi.list();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
.then(items => items.flat());
|
.then(items => items.flat());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(payload: IReleaseCreatePayload) {
|
async create(payload: ReleaseCreatePayload): Promise<ReleaseUpdateDetails> {
|
||||||
const response = await helmReleasesApi.create(payload);
|
const response = await helmReleasesApi.create(payload);
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) {
|
||||||
|
this.loadAll();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
async update(name: string, namespace: string, payload: ReleaseUpdatePayload): Promise<ReleaseUpdateDetails> {
|
||||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) {
|
||||||
|
this.loadAll();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollback(name: string, namespace: string, revision: number) {
|
async rollback(name: string, namespace: string, revision: number): Promise<KubeJsonApiData> {
|
||||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) {
|
||||||
|
this.loadAll();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(release: HelmRelease) {
|
async remove(release: HelmRelease): Promise<void> {
|
||||||
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
|
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeSelectedItems() {
|
async removeSelectedItems(): Promise<void> {
|
||||||
if (!this.selectedItems.length) return;
|
if (!this.selectedItems.length) {
|
||||||
return Promise.all(this.selectedItems.map(this.remove));
|
return;
|
||||||
|
}
|
||||||
|
await Promise.all(this.selectedItems.map(this.remove));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { observer } from "mobx-react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { releaseStore } from "./release.store";
|
import { releaseStore } from "./release.store";
|
||||||
import { IReleaseRouteParams, releaseURL } from "./release.route";
|
import { ReleaseRouteParams, releaseURL } from "./release.route";
|
||||||
import { HelmRelease } from "../../api/endpoints/helm-releases.api";
|
import { HelmRelease } from "../../api/endpoints/helm-releases.api";
|
||||||
import { ReleaseDetails } from "./release-details";
|
import { ReleaseDetails } from "./release-details";
|
||||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||||
@ -24,59 +24,58 @@ enum sortBy {
|
|||||||
updated = "update"
|
updated = "update"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IReleaseRouteParams> {
|
interface Props extends RouteComponentProps<ReleaseRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HelmReleases extends Component<Props> {
|
export class HelmReleases extends Component<Props> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
// Watch for secrets associated with releases and react to their changes
|
// Watch for secrets associated with releases and react to their changes
|
||||||
releaseStore.watch();
|
releaseStore.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
releaseStore.unwatch();
|
releaseStore.unwatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedRelease() {
|
get selectedRelease(): HelmRelease {
|
||||||
const { match: { params: { name, namespace } } } = this.props;
|
const { match: { params: { name, namespace } } } = this.props;
|
||||||
return releaseStore.items.find(release => {
|
return releaseStore.items.find(release => {
|
||||||
return release.getName() == name && release.getNs() == namespace;
|
return release.getName() == name && release.namespace == namespace;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetails = (item: HelmRelease) => {
|
showDetails = (item: HelmRelease): void => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
navigation.merge(releaseURL())
|
navigation.merge(releaseURL());
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
navigation.merge(releaseURL({
|
navigation.merge(releaseURL({
|
||||||
params: {
|
params: {
|
||||||
name: item.getName(),
|
name: item.getName(),
|
||||||
namespace: item.getNs()
|
namespace: item.namespace
|
||||||
}
|
}
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideDetails = () => {
|
hideDetails = (): void => {
|
||||||
this.showDetails(null);
|
this.showDetails(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
|
renderRemoveDialogMessage(selectedItems: HelmRelease[]): JSX.Element {
|
||||||
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
|
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Trans>Remove <b>{releaseNames}</b>?</Trans>
|
<Trans>Remove <b>{releaseNames}</b>?</Trans>
|
||||||
<p className="warning">
|
<p className="warning">
|
||||||
<Trans>Note: StatefulSet Volumes won't be deleted automatically</Trans>
|
<Trans>Note: StatefulSet Volumes won't be deleted automatically</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
@ -84,19 +83,19 @@ export class HelmReleases extends Component<Props> {
|
|||||||
store={releaseStore}
|
store={releaseStore}
|
||||||
dependentStores={[secretsStore]}
|
dependentStores={[secretsStore]}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (release: HelmRelease) => release.getName(),
|
[sortBy.name]: (release: HelmRelease): string => release.getName(),
|
||||||
[sortBy.namespace]: (release: HelmRelease) => release.getNs(),
|
[sortBy.namespace]: (release: HelmRelease): string => release.namespace,
|
||||||
[sortBy.revision]: (release: HelmRelease) => release.getRevision(),
|
[sortBy.revision]: (release: HelmRelease): number => release.revision,
|
||||||
[sortBy.chart]: (release: HelmRelease) => release.getChart(),
|
[sortBy.chart]: (release: HelmRelease): string => release.getChart(),
|
||||||
[sortBy.status]: (release: HelmRelease) => release.getStatus(),
|
[sortBy.status]: (release: HelmRelease): string => release.status,
|
||||||
[sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false),
|
[sortBy.updated]: (release: HelmRelease): string | number => release.getUpdated(false, false),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(release: HelmRelease) => release.getName(),
|
(release: HelmRelease): string => release.getName(),
|
||||||
(release: HelmRelease) => release.getNs(),
|
(release: HelmRelease): string => release.namespace,
|
||||||
(release: HelmRelease) => release.getChart(),
|
(release: HelmRelease): string => release.getChart(),
|
||||||
(release: HelmRelease) => release.getStatus(),
|
(release: HelmRelease): string => release.getStatus(),
|
||||||
(release: HelmRelease) => release.getVersion(),
|
(release: HelmRelease): string | number => release.getVersion(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Releases</Trans>}
|
renderHeaderTitle={<Trans>Releases</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -109,30 +108,30 @@ export class HelmReleases extends Component<Props> {
|
|||||||
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
||||||
{ title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated },
|
{ title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(release: HelmRelease) => {
|
renderTableContents={(release: HelmRelease): (string | number | React.ReactNode)[] => {
|
||||||
const version = release.getVersion();
|
const version = release.getVersion();
|
||||||
return [
|
return [
|
||||||
release.getName(),
|
release.getName(),
|
||||||
release.getNs(),
|
release.namespace,
|
||||||
release.getChart(),
|
release.getChart(),
|
||||||
release.getRevision(),
|
release.revision,
|
||||||
<>
|
<>
|
||||||
{version}
|
{version}
|
||||||
</>,
|
</>,
|
||||||
release.appVersion,
|
release.appVersion,
|
||||||
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
|
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
|
||||||
release.getUpdated(),
|
release.getUpdated(),
|
||||||
]
|
];
|
||||||
}}
|
}}
|
||||||
renderItemMenu={(release: HelmRelease) => {
|
renderItemMenu={(release: HelmRelease): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<HelmReleaseMenu
|
<HelmReleaseMenu
|
||||||
release={release}
|
release={release}
|
||||||
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
|
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
|
customizeRemoveDialog={(selectedItems: HelmRelease[]): {message: JSX.Element} => ({
|
||||||
message: this.renderRemoveDialogMessage(selectedItems)
|
message: this.renderRemoveDialogMessage(selectedItems)
|
||||||
})}
|
})}
|
||||||
detailsItem={this.selectedRelease}
|
detailsItem={this.selectedRelease}
|
||||||
|
|||||||
@ -24,10 +24,10 @@ export class Apps extends React.Component {
|
|||||||
url: releaseURL({ query }),
|
url: releaseURL({ query }),
|
||||||
path: releaseRoute.path,
|
path: releaseRoute.path,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const tabRoutes = Apps.tabRoutes;
|
const tabRoutes = Apps.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Apps" tabs={tabRoutes}>
|
<MainLayout className="Apps" tabs={tabRoutes}>
|
||||||
@ -36,6 +36,6 @@ export class Apps extends React.Component {
|
|||||||
<Redirect to={tabRoutes[0].url}/>
|
<Redirect to={tabRoutes[0].url}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./cluster-issues.scss"
|
import "./cluster-issues.scss";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -20,7 +20,7 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IWarning extends ItemObject {
|
interface Warning extends ItemObject {
|
||||||
kind: string;
|
kind: string;
|
||||||
message: string;
|
message: string;
|
||||||
selfLink: string;
|
selfLink: string;
|
||||||
@ -34,16 +34,16 @@ enum sortBy {
|
|||||||
@observer
|
@observer
|
||||||
export class ClusterIssues extends React.Component<Props> {
|
export class ClusterIssues extends React.Component<Props> {
|
||||||
private sortCallbacks = {
|
private sortCallbacks = {
|
||||||
[sortBy.type]: (warning: IWarning) => warning.kind,
|
[sortBy.type]: (warning: Warning): string => warning.kind,
|
||||||
[sortBy.object]: (warning: IWarning) => warning.getName(),
|
[sortBy.object]: (warning: Warning): string => warning.getName(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@computed get warnings() {
|
@computed get warnings(): Warning[] {
|
||||||
const warnings: IWarning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
// Node bad conditions
|
// Node bad conditions
|
||||||
nodesStore.items.forEach(node => {
|
nodesStore.items.forEach(node => {
|
||||||
const { kind, selfLink, getId, getName } = node
|
const { kind, selfLink, getId, getName } = node;
|
||||||
node.getWarningConditions().forEach(({ message }) => {
|
node.getWarningConditions().forEach(({ message }) => {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
kind,
|
kind,
|
||||||
@ -51,8 +51,8 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
getName,
|
getName,
|
||||||
selfLink,
|
selfLink,
|
||||||
message,
|
message,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Warning events for Workloads
|
// Warning events for Workloads
|
||||||
@ -67,13 +67,13 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
kind,
|
kind,
|
||||||
selfLink: lookupApiLink(involvedObject, error),
|
selfLink: lookupApiLink(involvedObject, error),
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
getTableRow(uid: string) {
|
getTableRow(uid: string): JSX.Element {
|
||||||
const { warnings } = this;
|
const { warnings } = this;
|
||||||
const warning = warnings.find(warn => warn.getId() == uid);
|
const warning = warnings.find(warn => warn.getId() == uid);
|
||||||
const { getId, getName, message, kind, selfLink } = warning;
|
const { getId, getName, message, kind, selfLink } = warning;
|
||||||
@ -97,7 +97,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent(): JSX.Element {
|
||||||
const { warnings } = this;
|
const { warnings } = this;
|
||||||
if (!eventStore.isLoaded) {
|
if (!eventStore.isLoaded) {
|
||||||
return (
|
return (
|
||||||
@ -139,7 +139,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ClusterIssues flex column", this.props.className)}>
|
<div className={cssNames("ClusterIssues flex column", this.props.className)}>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
|
|||||||
@ -21,7 +21,9 @@ export const ClusterMetricSwitchers = observer(() => {
|
|||||||
asButtons
|
asButtons
|
||||||
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
||||||
value={metricNodeRole}
|
value={metricNodeRole}
|
||||||
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric}
|
onChange={(metric: MetricNodeRole): void => {
|
||||||
|
clusterStore.metricNodeRole = metric;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
|
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
|
||||||
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
|
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
|
||||||
@ -32,7 +34,9 @@ export const ClusterMetricSwitchers = observer(() => {
|
|||||||
asButtons
|
asButtons
|
||||||
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
||||||
value={metricType}
|
value={metricType}
|
||||||
onChange={(value: MetricType) => clusterStore.metricType = value}
|
onChange={(value: MetricType): void => {
|
||||||
|
clusterStore.metricType = value;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
|
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
|
||||||
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
|
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
|
||||||
|
|||||||
@ -34,13 +34,13 @@ export const ClusterMetrics = observer(() => {
|
|||||||
yAxes: [{
|
yAxes: [{
|
||||||
ticks: {
|
ticks: {
|
||||||
suggestedMax: cpuCapacity,
|
suggestedMax: cpuCapacity,
|
||||||
callback: (value) => value
|
callback: (value): any => value
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: ({ index }, data) => {
|
label: ({ index }, data): string => {
|
||||||
const value = data.datasets[0].data[index] as ChartPoint;
|
const value = data.datasets[0].data[index] as ChartPoint;
|
||||||
return value.y.toString();
|
return value.y.toString();
|
||||||
}
|
}
|
||||||
@ -52,13 +52,13 @@ export const ClusterMetrics = observer(() => {
|
|||||||
yAxes: [{
|
yAxes: [{
|
||||||
ticks: {
|
ticks: {
|
||||||
suggestedMax: memoryCapacity,
|
suggestedMax: memoryCapacity,
|
||||||
callback: (value: string) => !value ? 0 : bytesToUnits(parseInt(value))
|
callback: (value: string): string | number => !value ? 0 : bytesToUnits(parseInt(value))
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: ({ index }, data) => {
|
label: ({ index }, data): string => {
|
||||||
const value = data.datasets[0].data[index] as ChartPoint;
|
const value = data.datasets[0].data[index] as ChartPoint;
|
||||||
return bytesToUnits(parseInt(value.y as string), 3);
|
return bytesToUnits(parseInt(value.y as string), 3);
|
||||||
}
|
}
|
||||||
@ -67,12 +67,12 @@ export const ClusterMetrics = observer(() => {
|
|||||||
};
|
};
|
||||||
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
|
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
|
||||||
|
|
||||||
const renderMetrics = () => {
|
const renderMetrics = (): JSX.Element => {
|
||||||
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
|
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
if (!memoryCapacity || !cpuCapacity) {
|
if (!memoryCapacity || !cpuCapacity) {
|
||||||
return <ClusterNoMetrics className="empty"/>
|
return <ClusterNoMetrics className="empty"/>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
|
|||||||
@ -7,7 +7,7 @@ interface Props {
|
|||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClusterNoMetrics({ className }: Props) {
|
export function ClusterNoMetrics({ className }: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}>
|
<div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}>
|
||||||
<Icon material="info"/>
|
<Icon material="info"/>
|
||||||
|
|||||||
@ -17,16 +17,16 @@ import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
|||||||
export const ClusterPieCharts = observer(() => {
|
export const ClusterPieCharts = observer(() => {
|
||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const renderLimitWarning = () => {
|
const renderLimitWarning = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="node-warning flex gaps align-center">
|
<div className="node-warning flex gaps align-center">
|
||||||
<Icon material="info"/>
|
<Icon material="info"/>
|
||||||
<p><Trans>Specified limits are higher than node capacity!</Trans></p>
|
<p><Trans>Specified limits are higher than node capacity!</Trans></p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderCharts = () => {
|
const renderCharts = (): JSX.Element => {
|
||||||
const data = getMetricLastPoints(clusterStore.metrics);
|
const data = getMetricLastPoints(clusterStore.metrics);
|
||||||
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
|
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
|
||||||
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
|
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
|
||||||
@ -35,7 +35,9 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
const memoryLimitsOverload = memoryLimits > memoryCapacity;
|
const memoryLimitsOverload = memoryLimits > memoryCapacity;
|
||||||
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
||||||
|
|
||||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null;
|
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const cpuData: ChartData = {
|
const cpuData: ChartData = {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@ -168,9 +170,9 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = (): JSX.Element => {
|
||||||
const { masterNodes, workerNodes } = nodesStore;
|
const { masterNodes, workerNodes } = nodesStore;
|
||||||
const { metricNodeRole, metricsLoaded } = clusterStore;
|
const { metricNodeRole, metricsLoaded } = clusterStore;
|
||||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||||
@ -194,11 +196,11 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
return <ClusterNoMetrics className="empty"/>;
|
return <ClusterNoMetrics className="empty"/>;
|
||||||
}
|
}
|
||||||
return renderCharts();
|
return renderCharts();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ClusterPieCharts flex">
|
<div className="ClusterPieCharts flex">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
@ -3,6 +3,6 @@ import { buildURL } from "../../navigation";
|
|||||||
|
|
||||||
export const clusterRoute: RouteProps = {
|
export const clusterRoute: RouteProps = {
|
||||||
path: "/cluster"
|
path: "/cluster"
|
||||||
}
|
};
|
||||||
|
|
||||||
export const clusterURL = buildURL(clusterRoute.path)
|
export const clusterURL = buildURL(clusterRoute.path);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { observable, reaction, when } from "mobx";
|
import { observable, reaction, when } from "mobx";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
|
import { Cluster, clusterApi, ClusterMetrics } from "../../api/endpoints";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, StorageHelper } from "../../utils";
|
||||||
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
import { MetricsReqParams, normalizeMetrics, Metrics } from "../../api/endpoints/metrics.api";
|
||||||
import { nodesStore } from "../+nodes/nodes.store";
|
import { nodesStore } from "../+nodes/nodes.store";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ export enum MetricNodeRole {
|
|||||||
export class ClusterStore extends KubeObjectStore<Cluster> {
|
export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||||
api = clusterApi
|
api = clusterApi
|
||||||
|
|
||||||
@observable metrics: Partial<IClusterMetrics> = {};
|
@observable metrics: Partial<ClusterMetrics> = {};
|
||||||
@observable liveMetrics: Partial<IClusterMetrics> = {};
|
@observable liveMetrics: Partial<ClusterMetrics> = {};
|
||||||
@observable metricsLoaded = false;
|
@observable metricsLoaded = false;
|
||||||
@observable metricType: MetricType;
|
@observable metricType: MetricType;
|
||||||
@observable metricNodeRole: MetricNodeRole;
|
@observable metricNodeRole: MetricNodeRole;
|
||||||
@ -31,18 +31,20 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
|||||||
this.resetMetrics();
|
this.resetMetrics();
|
||||||
|
|
||||||
// sync user setting with local storage
|
// sync user setting with local storage
|
||||||
const storage = createStorage("cluster_metric_switchers", {});
|
const storage = new StorageHelper("cluster_metric_switchers", {});
|
||||||
Object.assign(this, storage.get());
|
Object.assign(this, storage.get());
|
||||||
reaction(() => {
|
reaction(() => {
|
||||||
const { metricType, metricNodeRole } = this;
|
const { metricType, metricNodeRole } = this;
|
||||||
return { metricType, metricNodeRole }
|
return { metricType, metricNodeRole };
|
||||||
},
|
},
|
||||||
settings => storage.set(settings)
|
settings => storage.set(settings)
|
||||||
);
|
);
|
||||||
|
|
||||||
// auto-update metrics
|
// auto-update metrics
|
||||||
reaction(() => this.metricNodeRole, () => {
|
reaction(() => this.metricNodeRole, () => {
|
||||||
if (!this.metricsLoaded) return;
|
if (!this.metricsLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.metrics = {};
|
this.metrics = {};
|
||||||
this.liveMetrics = {};
|
this.liveMetrics = {};
|
||||||
this.metricsLoaded = false;
|
this.metricsLoaded = false;
|
||||||
@ -52,29 +54,33 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
|||||||
// check which node type to select
|
// check which node type to select
|
||||||
reaction(() => nodesStore.items.length, () => {
|
reaction(() => nodesStore.items.length, () => {
|
||||||
const { masterNodes, workerNodes } = nodesStore;
|
const { masterNodes, workerNodes } = nodesStore;
|
||||||
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
|
if (!masterNodes.length) {
|
||||||
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
|
this.metricNodeRole = MetricNodeRole.WORKER;
|
||||||
|
}
|
||||||
|
if (!workerNodes.length) {
|
||||||
|
this.metricNodeRole = MetricNodeRole.MASTER;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMetrics(params?: IMetricsReqParams) {
|
async loadMetrics(params?: MetricsReqParams): Promise<ClusterMetrics<Metrics>> {
|
||||||
await when(() => nodesStore.isLoaded);
|
await when(() => nodesStore.isLoaded);
|
||||||
const { masterNodes, workerNodes } = nodesStore;
|
const { masterNodes, workerNodes } = nodesStore;
|
||||||
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
||||||
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
|
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllMetrics() {
|
async getAllMetrics(): Promise<void> {
|
||||||
await this.getMetrics();
|
await this.getMetrics();
|
||||||
await this.getLiveMetrics();
|
await this.getLiveMetrics();
|
||||||
this.metricsLoaded = true;
|
this.metricsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetrics() {
|
async getMetrics(): Promise<void> {
|
||||||
this.metrics = await this.loadMetrics();
|
this.metrics = await this.loadMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLiveMetrics() {
|
async getLiveMetrics(): Promise<void> {
|
||||||
const step = 3;
|
const step = 3;
|
||||||
const range = 15;
|
const range = 15;
|
||||||
const end = Date.now() / 1000;
|
const end = Date.now() / 1000;
|
||||||
@ -82,25 +88,25 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
|||||||
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
|
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
|
getMetricsValues(source: Partial<ClusterMetrics>): [number, string][] {
|
||||||
switch (this.metricType) {
|
switch (this.metricType) {
|
||||||
case MetricType.CPU:
|
case MetricType.CPU:
|
||||||
return normalizeMetrics(source.cpuUsage).data.result[0].values
|
return normalizeMetrics(source.cpuUsage).data.result[0].values;
|
||||||
case MetricType.MEMORY:
|
case MetricType.MEMORY:
|
||||||
return normalizeMetrics(source.memoryUsage).data.result[0].values
|
return normalizeMetrics(source.memoryUsage).data.result[0].values;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetMetrics() {
|
resetMetrics(): void {
|
||||||
this.metrics = {};
|
this.metrics = {};
|
||||||
this.metricsLoaded = false;
|
this.metricsLoaded = false;
|
||||||
this.metricType = MetricType.CPU;
|
this.metricType = MetricType.CPU;
|
||||||
this.metricNodeRole = MetricNodeRole.WORKER;
|
this.metricNodeRole = MetricNodeRole.WORKER;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset(): void {
|
||||||
super.reset();
|
super.reset();
|
||||||
this.resetMetrics();
|
this.resetMetrics();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./cluster.scss"
|
import "./cluster.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, reaction } from "mobx";
|
import { computed, reaction } from "mobx";
|
||||||
@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
|||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import { ClusterIssues } from "./cluster-issues";
|
import { ClusterIssues } from "./cluster-issues";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { cssNames, interval, isElectron } from "../../utils";
|
import { cssNames, IntervalManager, isElectron } from "../../utils";
|
||||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||||
import { ClusterMetrics } from "./cluster-metrics";
|
import { ClusterMetrics } from "./cluster-metrics";
|
||||||
import { nodesStore } from "../+nodes/nodes.store";
|
import { nodesStore } from "../+nodes/nodes.store";
|
||||||
@ -18,16 +18,16 @@ import { isAllowedResource } from "../../api/rbac";
|
|||||||
@observer
|
@observer
|
||||||
export class Cluster extends React.Component {
|
export class Cluster extends React.Component {
|
||||||
private watchers = [
|
private watchers = [
|
||||||
interval(60, () => clusterStore.getMetrics()),
|
new IntervalManager(60, () => clusterStore.getMetrics()),
|
||||||
interval(20, () => eventStore.loadAll())
|
new IntervalManager(20, () => eventStore.loadAll())
|
||||||
];
|
];
|
||||||
|
|
||||||
private dependentStores = [nodesStore, podsStore];
|
private dependentStores = [nodesStore, podsStore];
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount(): Promise<void> {
|
||||||
const { dependentStores } = this;
|
const { dependentStores } = this;
|
||||||
if (!isAllowedResource("nodes")) {
|
if (!isAllowedResource("nodes")) {
|
||||||
dependentStores.splice(dependentStores.indexOf(nodesStore), 1)
|
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
|
||||||
}
|
}
|
||||||
this.watchers.forEach(watcher => watcher.start(true));
|
this.watchers.forEach(watcher => watcher.start(true));
|
||||||
|
|
||||||
@ -38,22 +38,22 @@ export class Cluster extends React.Component {
|
|||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
...dependentStores.map(store => store.subscribe()),
|
...dependentStores.map(store => store.subscribe()),
|
||||||
() => this.watchers.forEach(watcher => watcher.stop()),
|
(): void => this.watchers.forEach(watcher => watcher.stop()),
|
||||||
reaction(
|
reaction(
|
||||||
() => clusterStore.metricNodeRole,
|
() => clusterStore.metricNodeRole,
|
||||||
() => this.watchers.forEach(watcher => watcher.restart())
|
() => this.watchers.forEach(watcher => watcher.restart())
|
||||||
)
|
)
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get isLoaded() {
|
@computed get isLoaded(): boolean {
|
||||||
return (
|
return (
|
||||||
nodesStore.isLoaded &&
|
nodesStore.isLoaded &&
|
||||||
podsStore.isLoaded
|
podsStore.isLoaded
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { isLoaded } = this;
|
const { isLoaded } = this;
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
@ -68,6 +68,6 @@ export class Cluster extends React.Component {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./cluster.routes"
|
export * from "./cluster.routes";
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { DrawerItem, DrawerTitle } from "../drawer";
|
|||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
|
import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, HpaMetric } from "../../api/endpoints/hpa.api";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
@ -21,10 +21,10 @@ interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HpaDetails extends React.Component<Props> {
|
export class HpaDetails extends React.Component<Props> {
|
||||||
renderMetrics() {
|
renderMetrics(): JSX.Element {
|
||||||
const { object: hpa } = this.props;
|
const { object: hpa } = this.props;
|
||||||
|
|
||||||
const renderName = (metric: IHpaMetric) => {
|
const renderName = (metric: HpaMetric): JSX.Element => {
|
||||||
switch (metric.type) {
|
switch (metric.type) {
|
||||||
case HpaMetricType.Resource:
|
case HpaMetricType.Resource:
|
||||||
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
|
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
|
||||||
@ -51,7 +51,7 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
@ -60,7 +60,7 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
<TableCell className="metrics"><Trans>Current / Target</Trans></TableCell>
|
<TableCell className="metrics"><Trans>Current / Target</Trans></TableCell>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{
|
{
|
||||||
hpa.getMetrics().map((metric, index) => {
|
hpa.spec.metrics.map((metric, index) => {
|
||||||
const name = renderName(metric);
|
const name = renderName(metric);
|
||||||
const values = hpa.getMetricValues(metric);
|
const values = hpa.getMetricValues(metric);
|
||||||
return (
|
return (
|
||||||
@ -68,16 +68,18 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
<TableCell className="name">{name}</TableCell>
|
<TableCell className="name">{name}</TableCell>
|
||||||
<TableCell className="metrics">{values}</TableCell>
|
<TableCell className="metrics">{values}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { object: hpa } = this.props;
|
const { object: hpa } = this.props;
|
||||||
if (!hpa) return;
|
if (!hpa) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { scaleTargetRef } = hpa.spec;
|
const { scaleTargetRef } = hpa.spec;
|
||||||
return (
|
return (
|
||||||
<div className="HpaDetails">
|
<div className="HpaDetails">
|
||||||
@ -92,20 +94,22 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem name={<Trans>Min Pods</Trans>}>
|
<DrawerItem name={<Trans>Min Pods</Trans>}>
|
||||||
{hpa.getMinPods()}
|
{hpa.spec.minReplicas}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem name={<Trans>Max Pods</Trans>}>
|
<DrawerItem name={<Trans>Max Pods</Trans>}>
|
||||||
{hpa.getMaxPods()}
|
{hpa.spec.maxReplicas}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem name={<Trans>Replicas</Trans>}>
|
<DrawerItem name={<Trans>Replicas</Trans>}>
|
||||||
{hpa.getReplicas()}
|
{hpa.status.currentReplicas}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
|
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
|
||||||
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||||
if (!isReady) return null;
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
@ -113,7 +117,7 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
className={cssNames({ [type.toLowerCase()]: isReady })}
|
className={cssNames({ [type.toLowerCase()]: isReady })}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
|
|||||||
|
|
||||||
export const hpaRoute: RouteProps = {
|
export const hpaRoute: RouteProps = {
|
||||||
path: "/hpa"
|
path: "/hpa"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface HpaRouteParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHpaRouteParams {
|
export const hpaURL = buildURL<HpaRouteParams>(hpaRoute.path);
|
||||||
}
|
|
||||||
|
|
||||||
export const hpaURL = buildURL<IHpaRouteParams>(hpaRoute.path)
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./hpa.scss"
|
import "./hpa.scss";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
import { IHpaRouteParams } from "./hpa.route";
|
import { HpaRouteParams } from "./hpa.route";
|
||||||
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
|
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
|
||||||
import { hpaStore } from "./hpa.store";
|
import { hpaStore } from "./hpa.store";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
@ -22,32 +22,32 @@ enum sortBy {
|
|||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IHpaRouteParams> {
|
interface Props extends RouteComponentProps<HpaRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HorizontalPodAutoscalers extends React.Component<Props> {
|
export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||||
getTargets(hpa: HorizontalPodAutoscaler) {
|
getTargets(hpa: HorizontalPodAutoscaler): JSX.Element {
|
||||||
const metrics = hpa.getMetrics();
|
const { metrics } = hpa.spec;
|
||||||
const metricsRemainCount = metrics.length - 1;
|
const metricsRemainCount = metrics.length - 1;
|
||||||
const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null;
|
const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null;
|
||||||
const metricValues = hpa.getMetricValues(metrics[0]);
|
const metricValues = hpa.getMetricValues(metrics[0]);
|
||||||
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
|
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="HorizontalPodAutoscalers" store={hpaStore}
|
className="HorizontalPodAutoscalers" store={hpaStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: HorizontalPodAutoscaler) => item.getName(),
|
[sortBy.name]: (item: HorizontalPodAutoscaler): string => item.getName(),
|
||||||
[sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
|
[sortBy.namespace]: (item: HorizontalPodAutoscaler): string => item.getNs(),
|
||||||
[sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
|
[sortBy.minPods]: (item: HorizontalPodAutoscaler): number => item.spec.minReplicas,
|
||||||
[sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
|
[sortBy.maxPods]: (item: HorizontalPodAutoscaler): number => item.spec.maxReplicas,
|
||||||
[sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
|
[sortBy.replicas]: (item: HorizontalPodAutoscaler): number => item.status.currentReplicas
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: HorizontalPodAutoscaler) => item.getSearchFields()
|
(item: HorizontalPodAutoscaler): string[] => item.getSearchFields()
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Horizontal Pod Autoscalers</Trans>}
|
renderHeaderTitle={<Trans>Horizontal Pod Autoscalers</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -60,16 +60,18 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
|||||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||||
{ title: <Trans>Status</Trans>, className: "status" },
|
{ title: <Trans>Status</Trans>, className: "status" },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(hpa: HorizontalPodAutoscaler) => [
|
renderTableContents={(hpa: HorizontalPodAutoscaler): (string | number | React.ReactNode)[] => [
|
||||||
hpa.getName(),
|
hpa.getName(),
|
||||||
hpa.getNs(),
|
hpa.getNs(),
|
||||||
this.getTargets(hpa),
|
this.getTargets(hpa),
|
||||||
hpa.getMinPods(),
|
hpa.spec.minReplicas,
|
||||||
hpa.getMaxPods(),
|
hpa.spec.maxReplicas,
|
||||||
hpa.getReplicas(),
|
hpa.status.currentReplicas,
|
||||||
hpa.getAge(),
|
hpa.getAge(),
|
||||||
hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||||
if (!isReady) return null;
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
@ -77,23 +79,23 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
|||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
className={cssNames(type.toLowerCase())}
|
className={cssNames(type.toLowerCase())}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
renderItemMenu={(item: HorizontalPodAutoscaler) => {
|
renderItemMenu={(item: HorizontalPodAutoscaler): JSX.Element => {
|
||||||
return <HpaMenu object={item}/>
|
return <HpaMenu object={item}/>;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>) {
|
export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectMenu {...props}/>
|
<KubeObjectMenu {...props}/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apiManager.registerViews(hpaApi, {
|
apiManager.registerViews(hpaApi, {
|
||||||
Menu: HpaMenu,
|
Menu: HpaMenu,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./hpa"
|
export * from "./hpa";
|
||||||
export * from "./hpa-details"
|
export * from "./hpa-details";
|
||||||
export * from "./hpa.route"
|
export * from "./hpa.route";
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
@observable isSaving = false;
|
@observable isSaving = false;
|
||||||
@observable data = observable.map();
|
@observable data = observable.map();
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount(): void {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const { object: configMap } = this.props;
|
const { object: configMap } = this.props;
|
||||||
@ -31,10 +31,10 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
this.data.replace(configMap.data); // refresh
|
this.data.replace(configMap.data); // refresh
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
save = async () => {
|
save = async (): Promise<void> => {
|
||||||
const { object: configMap } = this.props;
|
const { object: configMap } = this.props;
|
||||||
try {
|
try {
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
@ -49,9 +49,11 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { object: configMap } = this.props;
|
const { object: configMap } = this.props;
|
||||||
if (!configMap) return null;
|
if (!configMap) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const data = Object.entries(this.data.toJSON());
|
const data = Object.entries(this.data.toJSON());
|
||||||
return (
|
return (
|
||||||
<div className="ConfigMapDetails">
|
<div className="ConfigMapDetails">
|
||||||
@ -71,11 +73,13 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
theme="round-black"
|
theme="round-black"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={v => this.data.set(name, v)}
|
onChange={(v): void => {
|
||||||
|
this.data.set(name, v);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
<Button
|
<Button
|
||||||
@ -96,4 +100,4 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
apiManager.registerViews(configMapApi, {
|
apiManager.registerViews(configMapApi, {
|
||||||
Details: ConfigMapDetails
|
Details: ConfigMapDetails
|
||||||
})
|
});
|
||||||
@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
|
|||||||
|
|
||||||
export const configMapsRoute: RouteProps = {
|
export const configMapsRoute: RouteProps = {
|
||||||
path: "/configmaps"
|
path: "/configmaps"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ConfigMapsRouteParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigMapsRouteParams {
|
export const configMapsURL = buildURL<ConfigMapsRouteParams>(configMapsRoute.path);
|
||||||
}
|
|
||||||
|
|
||||||
export const configMapsURL = buildURL<IConfigMapsRouteParams>(configMapsRoute.path);
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./config-maps.scss"
|
import "./config-maps.scss";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -8,7 +8,7 @@ import { configMapsStore } from "./config-maps.store";
|
|||||||
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
|
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
|
||||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
import { IConfigMapsRouteParams } from "./config-maps.route";
|
import { ConfigMapsRouteParams } from "./config-maps.route";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
@ -18,24 +18,24 @@ enum sortBy {
|
|||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IConfigMapsRouteParams> {
|
interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ConfigMaps extends React.Component<Props> {
|
export class ConfigMaps extends React.Component<Props> {
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="ConfigMaps" store={configMapsStore}
|
className="ConfigMaps" store={configMapsStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: ConfigMap) => item.getName(),
|
[sortBy.name]: (item: ConfigMap): string => item.getName(),
|
||||||
[sortBy.namespace]: (item: ConfigMap) => item.getNs(),
|
[sortBy.namespace]: (item: ConfigMap): string => item.getNs(),
|
||||||
[sortBy.keys]: (item: ConfigMap) => item.getKeys(),
|
[sortBy.keys]: (item: ConfigMap): string[] => item.getKeys(),
|
||||||
[sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp,
|
[sortBy.age]: (item: ConfigMap): string => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: ConfigMap) => item.getSearchFields(),
|
(item: ConfigMap): string[] => item.getSearchFields(),
|
||||||
(item: ConfigMap) => item.getKeys()
|
(item: ConfigMap): string[] => item.getKeys()
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Config Maps</Trans>}
|
renderHeaderTitle={<Trans>Config Maps</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -44,26 +44,26 @@ export class ConfigMaps extends React.Component<Props> {
|
|||||||
{ title: <Trans>Keys</Trans>, className: "keys", sortBy: sortBy.keys },
|
{ title: <Trans>Keys</Trans>, className: "keys", sortBy: sortBy.keys },
|
||||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(configMap: ConfigMap) => [
|
renderTableContents={(configMap: ConfigMap): (string | number)[] => [
|
||||||
configMap.getName(),
|
configMap.getName(),
|
||||||
configMap.getNs(),
|
configMap.getNs(),
|
||||||
configMap.getKeys().join(", "),
|
configMap.getKeys().join(", "),
|
||||||
configMap.getAge(),
|
configMap.getAge(),
|
||||||
]}
|
]}
|
||||||
renderItemMenu={(item: ConfigMap) => {
|
renderItemMenu={(item: ConfigMap): JSX.Element => {
|
||||||
return <ConfigMapMenu object={item}/>
|
return <ConfigMapMenu object={item}/>;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>) {
|
export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectMenu {...props}/>
|
<KubeObjectMenu {...props}/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apiManager.registerViews(configMapApi, {
|
apiManager.registerViews(configMapApi, {
|
||||||
Menu: ConfigMapMenu,
|
Menu: ConfigMapMenu,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./config-maps.route"
|
export * from "./config-maps.route";
|
||||||
export * from "./config-maps"
|
export * from "./config-maps";
|
||||||
export * from "./config-map-details"
|
export * from "./config-map-details";
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { Dialog, DialogProps } from "../dialog";
|
|||||||
import { Wizard, WizardStep } from "../wizard";
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { systemName } from "../input/input.validators";
|
import { systemName } from "../input/input.validators";
|
||||||
import { IResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
import { ResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@ -20,11 +20,16 @@ import { SubTitle } from "../layout/sub-title";
|
|||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuotaOption {
|
||||||
|
label: string | JSX.Element;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class AddQuotaDialog extends React.Component<Props> {
|
export class AddQuotaDialog extends React.Component<Props> {
|
||||||
@observable static isOpen = false;
|
@observable static isOpen = false;
|
||||||
|
|
||||||
static defaultQuotas: IResourceQuotaValues = {
|
static defaultQuotas: ResourceQuotaValues = {
|
||||||
"limits.cpu": "",
|
"limits.cpu": "",
|
||||||
"limits.memory": "",
|
"limits.memory": "",
|
||||||
"requests.cpu": "",
|
"requests.cpu": "",
|
||||||
@ -53,43 +58,45 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
@observable namespace = this.defaultNamespace;
|
@observable namespace = this.defaultNamespace;
|
||||||
@observable quotas = AddQuotaDialog.defaultQuotas;
|
@observable quotas = AddQuotaDialog.defaultQuotas;
|
||||||
|
|
||||||
static open() {
|
static open(): void {
|
||||||
AddQuotaDialog.isOpen = true;
|
AddQuotaDialog.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static close() {
|
static close(): void {
|
||||||
AddQuotaDialog.isOpen = false;
|
AddQuotaDialog.isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get quotaEntries() {
|
@computed get quotaEntries(): [string, string][] {
|
||||||
return Object.entries(this.quotas)
|
return Object.entries(this.quotas)
|
||||||
.filter(([type, value]) => !!value.trim());
|
.filter(([_type, value]) => !!value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get quotaOptions() {
|
@computed get quotaOptions(): QuotaOption[] {
|
||||||
return Object.keys(this.quotas).map(quota => {
|
return Object.keys(this.quotas).map(value => {
|
||||||
const isCompute = quota.endsWith(".cpu") || quota.endsWith(".memory");
|
const isCompute = value.endsWith(".cpu") || value.endsWith(".memory");
|
||||||
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
|
const isStorage = value.endsWith(".storage") || value === "persistentvolumeclaims";
|
||||||
const isCount = quota.startsWith("count/");
|
const isCount = value.startsWith("count/");
|
||||||
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
|
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
|
||||||
return {
|
return {
|
||||||
label: icon ? <span className="nobr"><Icon material={icon}/> {quota}</span> : quota,
|
label: icon ? <span className="nobr"><Icon material={icon}/> {value}</span> : value,
|
||||||
value: quota,
|
value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuota = () => {
|
setQuota = (): void => {
|
||||||
if (!this.quotaSelectValue) return;
|
if (!this.quotaSelectValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.quotas[this.quotaSelectValue] = this.quotaInputValue;
|
this.quotas[this.quotaSelectValue] = this.quotaInputValue;
|
||||||
this.quotaInputValue = "";
|
this.quotaInputValue = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
close = () => {
|
close = (): void => {
|
||||||
AddQuotaDialog.close();
|
AddQuotaDialog.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset = () => {
|
reset = (): void => {
|
||||||
this.quotaName = "";
|
this.quotaName = "";
|
||||||
this.quotaSelectValue = "";
|
this.quotaSelectValue = "";
|
||||||
this.quotaInputValue = "";
|
this.quotaInputValue = "";
|
||||||
@ -97,10 +104,10 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
this.quotas = AddQuotaDialog.defaultQuotas;
|
this.quotas = AddQuotaDialog.defaultQuotas;
|
||||||
}
|
}
|
||||||
|
|
||||||
addQuota = async () => {
|
addQuota = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { quotaName, namespace } = this;
|
const { quotaName, namespace } = this;
|
||||||
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
|
const quotas = this.quotaEntries.reduce<ResourceQuotaValues>((quotas, [name, value]) => {
|
||||||
quotas[name] = value;
|
quotas[name] = value;
|
||||||
return quotas;
|
return quotas;
|
||||||
}, {});
|
}, {});
|
||||||
@ -115,7 +122,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputQuota = (evt: React.KeyboardEvent) => {
|
onInputQuota = (evt: React.KeyboardEvent): void => {
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
this.setQuota();
|
this.setQuota();
|
||||||
@ -124,7 +131,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { ...dialogProps } = this.props;
|
const { ...dialogProps } = this.props;
|
||||||
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
|
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
|
||||||
return (
|
return (
|
||||||
@ -146,7 +153,9 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
required autoFocus
|
required autoFocus
|
||||||
placeholder={_i18n._(t`ResourceQuota name`)}
|
placeholder={_i18n._(t`ResourceQuota name`)}
|
||||||
validators={systemName}
|
validators={systemName}
|
||||||
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
|
value={this.quotaName} onChange={(v): void => {
|
||||||
|
this.quotaName = v.toLowerCase();
|
||||||
|
}}
|
||||||
className="box grow"
|
className="box grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -157,7 +166,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
placeholder={_i18n._(t`Namespace`)}
|
placeholder={_i18n._(t`Namespace`)}
|
||||||
themeName="light"
|
themeName="light"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
onChange={({ value }) => this.namespace = value}
|
onChange={({ value }): void => this.namespace = value}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubTitle title={<Trans>Values</Trans>}/>
|
<SubTitle title={<Trans>Values</Trans>}/>
|
||||||
@ -168,13 +177,15 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
placeholder={_i18n._(t`Select a quota..`)}
|
placeholder={_i18n._(t`Select a quota..`)}
|
||||||
options={this.quotaOptions}
|
options={this.quotaOptions}
|
||||||
value={this.quotaSelectValue}
|
value={this.quotaSelectValue}
|
||||||
onChange={({ value }) => this.quotaSelectValue = value}
|
onChange={({ value }): void => this.quotaSelectValue = value}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
placeholder={_i18n._(t`Value`)}
|
placeholder={_i18n._(t`Value`)}
|
||||||
value={this.quotaInputValue}
|
value={this.quotaInputValue}
|
||||||
onChange={v => this.quotaInputValue = v}
|
onChange={(v): void => {
|
||||||
|
this.quotaInputValue = v;
|
||||||
|
}}
|
||||||
onKeyDown={this.onInputQuota}
|
onKeyDown={this.onInputQuota}
|
||||||
className="box grow"
|
className="box grow"
|
||||||
/>
|
/>
|
||||||
@ -191,14 +202,16 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
<div key={quota} className="quota flex gaps inline align-center">
|
<div key={quota} className="quota flex gaps inline align-center">
|
||||||
<div className="name">{quota}</div>
|
<div className="name">{quota}</div>
|
||||||
<div className="value">{value}</div>
|
<div className="value">{value}</div>
|
||||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""}/>
|
<Icon material="clear" onClick={(): void => {
|
||||||
|
this.quotas[quota] = "";
|
||||||
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
</Wizard>
|
</Wizard>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./resource-quotas.route"
|
export * from "./resource-quotas.route";
|
||||||
export * from "./resource-quotas"
|
export * from "./resource-quotas";
|
||||||
export * from "./resource-quota-details"
|
export * from "./resource-quota-details";
|
||||||
|
|||||||
@ -17,22 +17,26 @@ interface Props extends KubeObjectDetailsProps<ResourceQuota> {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ResourceQuotaDetails extends React.Component<Props> {
|
export class ResourceQuotaDetails extends React.Component<Props> {
|
||||||
renderQuotas = (quota: ResourceQuota) => {
|
renderQuotas = (quota: ResourceQuota): JSX.Element[] => {
|
||||||
const { hard, used } = quota.status
|
const { hard, used } = quota.status;
|
||||||
if (!hard || !used) return null
|
if (!hard || !used) {
|
||||||
const transformUnit = (name: string, value: string) => {
|
return null;
|
||||||
|
}
|
||||||
|
const transformUnit = (name: string, value: string): number => {
|
||||||
if (name.includes("memory") || name.includes("storage")) {
|
if (name.includes("memory") || name.includes("storage")) {
|
||||||
return unitsToBytes(value)
|
return unitsToBytes(value);
|
||||||
}
|
}
|
||||||
if (name.includes("cpu")) {
|
if (name.includes("cpu")) {
|
||||||
return cpuUnitsToNumber(value)
|
return cpuUnitsToNumber(value);
|
||||||
}
|
}
|
||||||
return parseInt(value)
|
return parseInt(value);
|
||||||
}
|
};
|
||||||
return Object.entries(hard).map(([name, value]) => {
|
return Object.entries(hard).map(([name, value]) => {
|
||||||
if (!used[name]) return null
|
if (!used[name]) {
|
||||||
const current = transformUnit(name, used[name])
|
return null;
|
||||||
const max = transformUnit(name, value)
|
}
|
||||||
|
const current = transformUnit(name, used[name]);
|
||||||
|
const max = transformUnit(name, value);
|
||||||
return (
|
return (
|
||||||
<div key={name} className={cssNames("param", kebabCase(name))}>
|
<div key={name} className={cssNames("param", kebabCase(name))}>
|
||||||
<span className="title">{name}</span>
|
<span className="title">{name}</span>
|
||||||
@ -45,13 +49,15 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { object: quota } = this.props;
|
const { object: quota } = this.props;
|
||||||
if (!quota) return null;
|
if (!quota) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="ResourceQuotaDetails">
|
<div className="ResourceQuotaDetails">
|
||||||
<KubeObjectMeta object={quota}/>
|
<KubeObjectMeta object={quota}/>
|
||||||
@ -91,4 +97,4 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
apiManager.registerViews(resourceQuotaApi, {
|
apiManager.registerViews(resourceQuotaApi, {
|
||||||
Details: ResourceQuotaDetails
|
Details: ResourceQuotaDetails
|
||||||
})
|
});
|
||||||
@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
|
|||||||
|
|
||||||
export const resourceQuotaRoute: RouteProps = {
|
export const resourceQuotaRoute: RouteProps = {
|
||||||
path: "/resourcequotas"
|
path: "/resourcequotas"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ResourceQuotaRouteParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IResourceQuotaRouteParams {
|
export const resourceQuotaURL = buildURL<ResourceQuotaRouteParams>(resourceQuotaRoute.path);
|
||||||
}
|
|
||||||
|
|
||||||
export const resourceQuotaURL = buildURL<IResourceQuotaRouteParams>(resourceQuotaRoute.path);
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object";
|
|||||||
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
||||||
import { AddQuotaDialog } from "./add-quota-dialog";
|
import { AddQuotaDialog } from "./add-quota-dialog";
|
||||||
import { resourceQuotaStore } from "./resource-quotas.store";
|
import { resourceQuotaStore } from "./resource-quotas.store";
|
||||||
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
|
import { ResourceQuotaRouteParams } from "./resource-quotas.route";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
@ -18,24 +18,24 @@ enum sortBy {
|
|||||||
age = "age"
|
age = "age"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IResourceQuotaRouteParams> {
|
interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ResourceQuotas extends React.Component<Props> {
|
export class ResourceQuotas extends React.Component<Props> {
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="ResourceQuotas" store={resourceQuotaStore}
|
className="ResourceQuotas" store={resourceQuotaStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: ResourceQuota) => item.getName(),
|
[sortBy.name]: (item: ResourceQuota): string => item.getName(),
|
||||||
[sortBy.namespace]: (item: ResourceQuota) => item.getNs(),
|
[sortBy.namespace]: (item: ResourceQuota): string => item.getNs(),
|
||||||
[sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp,
|
[sortBy.age]: (item: ResourceQuota): string => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: ResourceQuota) => item.getSearchFields(),
|
(item: ResourceQuota): string[] => item.getSearchFields(),
|
||||||
(item: ResourceQuota) => item.getName(),
|
(item: ResourceQuota): string => item.getName(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Resource Quotas</Trans>}
|
renderHeaderTitle={<Trans>Resource Quotas</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -43,16 +43,14 @@ export class ResourceQuotas extends React.Component<Props> {
|
|||||||
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(resourceQuota: ResourceQuota) => [
|
renderTableContents={(resourceQuota: ResourceQuota): (string | number)[] => [
|
||||||
resourceQuota.getName(),
|
resourceQuota.getName(),
|
||||||
resourceQuota.getNs(),
|
resourceQuota.getNs(),
|
||||||
resourceQuota.getAge(),
|
resourceQuota.getAge(),
|
||||||
]}
|
]}
|
||||||
renderItemMenu={(item: ResourceQuota) => {
|
renderItemMenu={(item: ResourceQuota): JSX.Element => <ResourceQuotaMenu object={item} />}
|
||||||
return <ResourceQuotaMenu object={item}/>
|
|
||||||
}}
|
|
||||||
addRemoveButtons={{
|
addRemoveButtons={{
|
||||||
onAdd: () => AddQuotaDialog.open(),
|
onAdd: (): void => AddQuotaDialog.open(),
|
||||||
addTooltip: <Trans>Create new ResourceQuota</Trans>
|
addTooltip: <Trans>Create new ResourceQuota</Trans>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -62,7 +60,7 @@ export class ResourceQuotas extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
|
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectMenu {...props}/>
|
<KubeObjectMenu {...props}/>
|
||||||
);
|
);
|
||||||
@ -70,4 +68,4 @@ export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
|
|||||||
|
|
||||||
apiManager.registerViews(resourceQuotaApi, {
|
apiManager.registerViews(resourceQuotaApi, {
|
||||||
Menu: ResourceQuotaMenu,
|
Menu: ResourceQuotaMenu,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./add-secret-dialog.scss"
|
import "./add-secret-dialog.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
@ -14,7 +14,7 @@ import { SubTitle } from "../layout/sub-title";
|
|||||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { IKubeObjectMetadata } from "../../api/kube-object";
|
import { KubeObjectMetadata } from "../../api/kube-object";
|
||||||
import { base64 } from "../../utils";
|
import { base64 } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { showDetails } from "../../navigation";
|
import { showDetails } from "../../navigation";
|
||||||
@ -23,34 +23,41 @@ import upperFirst from "lodash/upperFirst";
|
|||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISecretTemplateField {
|
interface SecretTemplateField {
|
||||||
key: string;
|
key: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISecretTemplate {
|
interface SecretTemplate {
|
||||||
[field: string]: ISecretTemplateField[];
|
[field: string]: SecretTemplateField[];
|
||||||
annotations?: ISecretTemplateField[];
|
annotations?: SecretTemplateField[];
|
||||||
labels?: ISecretTemplateField[];
|
labels?: SecretTemplateField[];
|
||||||
data?: ISecretTemplateField[];
|
data?: SecretTemplateField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ISecretField = keyof ISecretTemplate;
|
type SecretField = keyof SecretTemplate;
|
||||||
|
|
||||||
|
type RecursivePartial<T> = {
|
||||||
|
[P in keyof T]?:
|
||||||
|
T[P] extends (infer U)[] ? RecursivePartial<U>[] :
|
||||||
|
T[P] extends object ? RecursivePartial<T[P]> :
|
||||||
|
T[P];
|
||||||
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class AddSecretDialog extends React.Component<Props> {
|
export class AddSecretDialog extends React.Component<Props> {
|
||||||
@observable static isOpen = false;
|
@observable static isOpen = false;
|
||||||
|
|
||||||
static open() {
|
static open(): void {
|
||||||
AddSecretDialog.isOpen = true;
|
AddSecretDialog.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static close() {
|
static close(): void {
|
||||||
AddSecretDialog.isOpen = false;
|
AddSecretDialog.isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private secretTemplate: { [p: string]: ISecretTemplate } = {
|
private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
|
||||||
[SecretType.Opaque]: {},
|
[SecretType.Opaque]: {},
|
||||||
[SecretType.ServiceAccountToken]: {
|
[SecretType.ServiceAccountToken]: {
|
||||||
annotations: [
|
annotations: [
|
||||||
@ -60,7 +67,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
get types() {
|
get types(): SecretType[] {
|
||||||
return Object.keys(this.secretTemplate) as SecretType[];
|
return Object.keys(this.secretTemplate) as SecretType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,38 +76,39 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
@observable namespace = "default";
|
@observable namespace = "default";
|
||||||
@observable type = SecretType.Opaque;
|
@observable type = SecretType.Opaque;
|
||||||
|
|
||||||
reset = () => {
|
reset = (): void => {
|
||||||
this.name = "";
|
this.name = "";
|
||||||
this.secret = this.secretTemplate;
|
this.secret = this.secretTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
close = () => {
|
close = (): void => {
|
||||||
AddSecretDialog.close();
|
AddSecretDialog.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => {
|
private getDataFromFields = (fields: SecretTemplateField[] = [], processValue?: (val: string) => string): any => {
|
||||||
return fields.reduce<any>((data, field) => {
|
return fields.reduce<any>((data, field) => {
|
||||||
const { key, value } = field;
|
const { key, value } = field;
|
||||||
if (key) {
|
if (key) {
|
||||||
data[key] = processValue ? processValue(value) : value;
|
data[key] = processValue ? processValue(value) : value;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}, {})
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
createSecret = async () => {
|
createSecret = async (): Promise<void> => {
|
||||||
const { name, namespace, type } = this;
|
const { name, namespace, type } = this;
|
||||||
const { data = [], labels = [], annotations = [] } = this.secret[type];
|
const { data = [], labels = [], annotations = [] } = this.secret[type];
|
||||||
|
const metadata: Partial<KubeObjectMetadata> = {
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
annotations: this.getDataFromFields(annotations),
|
||||||
|
labels: this.getDataFromFields(labels),
|
||||||
|
};
|
||||||
const secret: Partial<Secret> = {
|
const secret: Partial<Secret> = {
|
||||||
type: type,
|
type: type,
|
||||||
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
|
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
|
||||||
metadata: {
|
metadata: metadata as KubeObjectMetadata,
|
||||||
name: name,
|
};
|
||||||
namespace: namespace,
|
|
||||||
annotations: this.getDataFromFields(annotations),
|
|
||||||
labels: this.getDataFromFields(labels),
|
|
||||||
} as IKubeObjectMetadata
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const newSecret = await secretsApi.create({ namespace, name }, secret);
|
const newSecret = await secretsApi.create({ namespace, name }, secret);
|
||||||
showDetails(newSecret.selfLink);
|
showDetails(newSecret.selfLink);
|
||||||
@ -111,18 +119,18 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addField = (field: ISecretField) => {
|
addField = (field: SecretField): void => {
|
||||||
const fields = this.secret[this.type][field] || [];
|
const fields = this.secret[this.type][field] || [];
|
||||||
fields.push({ key: "", value: "" });
|
fields.push({ key: "", value: "" });
|
||||||
this.secret[this.type][field] = fields;
|
this.secret[this.type][field] = fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeField = (field: ISecretField, index: number) => {
|
removeField = (field: SecretField, index: number): void => {
|
||||||
const fields = this.secret[this.type][field] || [];
|
const fields = this.secret[this.type][field] || [];
|
||||||
fields.splice(index, 1);
|
fields.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFields(field: ISecretField) {
|
renderFields(field: SecretField): JSX.Element {
|
||||||
const fields = this.secret[this.type][field] || [];
|
const fields = this.secret[this.type][field] || [];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -131,7 +139,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
small
|
small
|
||||||
tooltip={_i18n._(t`Add field`)}
|
tooltip={_i18n._(t`Add field`)}
|
||||||
material="add_circle_outline"
|
material="add_circle_outline"
|
||||||
onClick={() => this.addField(field)}
|
onClick={(): void => this.addField(field)}
|
||||||
/>
|
/>
|
||||||
</SubTitle>
|
</SubTitle>
|
||||||
<div className="secret-fields">
|
<div className="secret-fields">
|
||||||
@ -145,14 +153,18 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
title={key}
|
title={key}
|
||||||
tabIndex={required ? -1 : 0}
|
tabIndex={required ? -1 : 0}
|
||||||
readOnly={required}
|
readOnly={required}
|
||||||
value={key} onChange={v => item.key = v}
|
value={key} onChange={(v): void => {
|
||||||
|
item.key = v;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
multiLine maxRows={5}
|
multiLine maxRows={5}
|
||||||
required={required}
|
required={required}
|
||||||
className="value"
|
className="value"
|
||||||
placeholder={_i18n._(t`Value`)}
|
placeholder={_i18n._(t`Value`)}
|
||||||
value={value} onChange={v => item.value = v}
|
value={value} onChange={(v): void => {
|
||||||
|
item.value = v;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
small
|
small
|
||||||
@ -160,17 +172,17 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
tooltip={required ? <Trans>Required field</Trans> : <Trans>Remove field</Trans>}
|
tooltip={required ? <Trans>Required field</Trans> : <Trans>Remove field</Trans>}
|
||||||
className="remove-icon"
|
className="remove-icon"
|
||||||
material="remove_circle_outline"
|
material="remove_circle_outline"
|
||||||
onClick={() => this.removeField(field, index)}
|
onClick={(): void => this.removeField(field, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { ...dialogProps } = this.props;
|
const { ...dialogProps } = this.props;
|
||||||
const { namespace, name, type } = this;
|
const { namespace, name, type } = this;
|
||||||
const header = <h5><Trans>Create Secret</Trans></h5>;
|
const header = <h5><Trans>Create Secret</Trans></h5>;
|
||||||
@ -189,7 +201,9 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
autoFocus required
|
autoFocus required
|
||||||
placeholder={_i18n._(t`Name`)}
|
placeholder={_i18n._(t`Name`)}
|
||||||
validators={systemName}
|
validators={systemName}
|
||||||
value={name} onChange={v => this.name = v}
|
value={name} onChange={(v): void => {
|
||||||
|
this.name = v;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex auto gaps">
|
<div className="flex auto gaps">
|
||||||
@ -198,7 +212,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
<NamespaceSelect
|
<NamespaceSelect
|
||||||
themeName="light"
|
themeName="light"
|
||||||
value={namespace}
|
value={namespace}
|
||||||
onChange={({ value }) => this.namespace = value}
|
onChange={({ value }): void => this.namespace = value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="secret-type">
|
<div className="secret-type">
|
||||||
@ -206,7 +220,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
<Select
|
<Select
|
||||||
themeName="light"
|
themeName="light"
|
||||||
options={this.types}
|
options={this.types}
|
||||||
value={type} onChange={({ value }: SelectOption) => this.type = value}
|
value={type} onChange={({ value }: SelectOption): void => this.type = value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -216,6 +230,6 @@ export class AddSecretDialog extends React.Component<Props> {
|
|||||||
</WizardStep>
|
</WizardStep>
|
||||||
</Wizard>
|
</Wizard>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from "./secrets.route"
|
export * from "./secrets.route";
|
||||||
export * from "./secrets"
|
export * from "./secrets";
|
||||||
export * from "./secret-details"
|
export * from "./secret-details";
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class SecretDetails extends React.Component<Props> {
|
|||||||
@observable data: { [name: string]: string } = {};
|
@observable data: { [name: string]: string } = {};
|
||||||
@observable revealSecret: { [name: string]: boolean } = {};
|
@observable revealSecret: { [name: string]: boolean } = {};
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount(): void {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const { object: secret } = this.props;
|
const { object: secret } = this.props;
|
||||||
@ -36,10 +36,10 @@ export class SecretDetails extends React.Component<Props> {
|
|||||||
this.revealSecret = {};
|
this.revealSecret = {};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSecret = async () => {
|
saveSecret = async (): Promise<void> => {
|
||||||
const { object: secret } = this.props;
|
const { object: secret } = this.props;
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
try {
|
try {
|
||||||
@ -51,13 +51,15 @@ export class SecretDetails extends React.Component<Props> {
|
|||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
editData = (name: string, value: string, encoded: boolean) => {
|
editData = (name: string, value: string, encoded: boolean): void => {
|
||||||
this.data[name] = encoded ? value : base64.encode(value);
|
this.data[name] = encoded ? value : base64.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { object: secret } = this.props;
|
const { object: secret } = this.props;
|
||||||
if (!secret) return null;
|
if (!secret) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="SecretDetails">
|
<div className="SecretDetails">
|
||||||
<KubeObjectMeta object={secret}/>
|
<KubeObjectMeta object={secret}/>
|
||||||
@ -86,18 +88,20 @@ export class SecretDetails extends React.Component<Props> {
|
|||||||
theme="round-black"
|
theme="round-black"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
onChange={value => this.editData(name, value, !revealSecret)}
|
onChange={(value): void => this.editData(name, value, !revealSecret)}
|
||||||
/>
|
/>
|
||||||
{decodedVal && (
|
{decodedVal && (
|
||||||
<Icon
|
<Icon
|
||||||
material={`visibility${revealSecret ? "" : "_off"}`}
|
material={`visibility${revealSecret ? "" : "_off"}`}
|
||||||
tooltip={revealSecret ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
|
tooltip={revealSecret ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
|
||||||
onClick={() => this.revealSecret[name] = !revealSecret}
|
onClick={(): void => {
|
||||||
|
this.revealSecret[name] = !revealSecret;
|
||||||
|
}}
|
||||||
/>)
|
/>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
<Button
|
<Button
|
||||||
@ -115,4 +119,4 @@ export class SecretDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
apiManager.registerViews(secretsApi, {
|
apiManager.registerViews(secretsApi, {
|
||||||
Details: SecretDetails,
|
Details: SecretDetails,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
|
|||||||
|
|
||||||
export const secretsRoute: RouteProps = {
|
export const secretsRoute: RouteProps = {
|
||||||
path: "/secrets"
|
path: "/secrets"
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ISecretsRouteParams {
|
export interface SecretsRouteParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const secretsURL = buildURL(secretsRoute.path);
|
export const secretsURL = buildURL(secretsRoute.path);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import "./secrets.scss"
|
import "./secrets.scss";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -7,7 +7,7 @@ import { RouteComponentProps } from "react-router";
|
|||||||
import { Secret, secretsApi } from "../../api/endpoints";
|
import { Secret, secretsApi } from "../../api/endpoints";
|
||||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||||
import { AddSecretDialog } from "./add-secret-dialog";
|
import { AddSecretDialog } from "./add-secret-dialog";
|
||||||
import { ISecretsRouteParams } from "./secrets.route";
|
import { SecretsRouteParams } from "./secrets.route";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { secretsStore } from "./secrets.store";
|
import { secretsStore } from "./secrets.store";
|
||||||
@ -22,27 +22,27 @@ enum sortBy {
|
|||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ISecretsRouteParams> {
|
interface Props extends RouteComponentProps<SecretsRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Secrets extends React.Component<Props> {
|
export class Secrets extends React.Component<Props> {
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Secrets" store={secretsStore}
|
className="Secrets" store={secretsStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: Secret) => item.getName(),
|
[sortBy.name]: (item: Secret): string => item.getName(),
|
||||||
[sortBy.namespace]: (item: Secret) => item.getNs(),
|
[sortBy.namespace]: (item: Secret): string => item.getNs(),
|
||||||
[sortBy.labels]: (item: Secret) => item.getLabels(),
|
[sortBy.labels]: (item: Secret): string[] => item.getLabels(),
|
||||||
[sortBy.keys]: (item: Secret) => item.getKeys(),
|
[sortBy.keys]: (item: Secret): string[] => item.getKeys(),
|
||||||
[sortBy.type]: (item: Secret) => item.type,
|
[sortBy.type]: (item: Secret): string => item.type,
|
||||||
[sortBy.age]: (item: Secret) => item.metadata.creationTimestamp,
|
[sortBy.age]: (item: Secret): string => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: Secret) => item.getSearchFields(),
|
(item: Secret): string[] => item.getSearchFields(),
|
||||||
(item: Secret) => item.getKeys(),
|
(item: Secret): string[] => item.getKeys(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Secrets</Trans>}
|
renderHeaderTitle={<Trans>Secrets</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -53,7 +53,7 @@ export class Secrets extends React.Component<Props> {
|
|||||||
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
|
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
|
||||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(secret: Secret) => [
|
renderTableContents={(secret: Secret): (string | JSX.Element[] | number)[] => [
|
||||||
secret.getName(),
|
secret.getName(),
|
||||||
secret.getNs(),
|
secret.getNs(),
|
||||||
secret.getLabels().map(label => <Badge key={label} label={label}/>),
|
secret.getLabels().map(label => <Badge key={label} label={label}/>),
|
||||||
@ -61,11 +61,11 @@ export class Secrets extends React.Component<Props> {
|
|||||||
secret.type,
|
secret.type,
|
||||||
secret.getAge(),
|
secret.getAge(),
|
||||||
]}
|
]}
|
||||||
renderItemMenu={(item: Secret) => {
|
renderItemMenu={(item: Secret): JSX.Element => {
|
||||||
return <SecretMenu object={item}/>
|
return <SecretMenu object={item}/>;
|
||||||
}}
|
}}
|
||||||
addRemoveButtons={{
|
addRemoveButtons={{
|
||||||
onAdd: () => AddSecretDialog.open(),
|
onAdd: (): void => AddSecretDialog.open(),
|
||||||
addTooltip: <Trans>Create new Secret</Trans>
|
addTooltip: <Trans>Create new Secret</Trans>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -75,12 +75,12 @@ export class Secrets extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SecretMenu(props: KubeObjectMenuProps<Secret>) {
|
export function SecretMenu(props: KubeObjectMenuProps<Secret>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<KubeObjectMenu {...props}/>
|
<KubeObjectMenu {...props}/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apiManager.registerViews(secretsApi, {
|
apiManager.registerViews(secretsApi, {
|
||||||
Menu: SecretMenu,
|
Menu: SecretMenu,
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { RouteProps } from "react-router";
|
import { RouteProps } from "react-router";
|
||||||
import { configMapsURL } from "../+config-maps";
|
import { configMapsURL } from "../+config-maps";
|
||||||
import { Config } from "./config";
|
import { Config } from "./config";
|
||||||
import { IURLParams } from "../../navigation";
|
import { URLParams } from "../../navigation";
|
||||||
|
|
||||||
export const configRoute: RouteProps = {
|
export const configRoute: RouteProps = {
|
||||||
get path() {
|
get path() {
|
||||||
return Config.tabRoutes.map(({ path }) => path).flat()
|
return Config.tabRoutes.map(({ path }) => path).flat();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const configURL = (params?: IURLParams) => configMapsURL(params);
|
export const configURL = (params?: URLParams): string => configMapsURL(params);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
|
|||||||
import { configURL } from "./config.route";
|
import { configURL } from "./config.route";
|
||||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||||
import { buildURL } from "../../navigation";
|
import { buildURL } from "../../navigation";
|
||||||
import { isAllowedResource } from "../../api/rbac"
|
import { isAllowedResource } from "../../api/rbac";
|
||||||
|
|
||||||
export const certificatesURL = buildURL("/certificates");
|
export const certificatesURL = buildURL("/certificates");
|
||||||
export const issuersURL = buildURL("/issuers");
|
export const issuersURL = buildURL("/issuers");
|
||||||
@ -19,15 +19,15 @@ export const clusterIssuersURL = buildURL("/clusterissuers");
|
|||||||
@observer
|
@observer
|
||||||
export class Config extends React.Component {
|
export class Config extends React.Component {
|
||||||
static get tabRoutes(): TabRoute[] {
|
static get tabRoutes(): TabRoute[] {
|
||||||
const query = namespaceStore.getContextParams()
|
const query = namespaceStore.getContextParams();
|
||||||
const routes: TabRoute[] = []
|
const routes: TabRoute[] = [];
|
||||||
if (isAllowedResource("configmaps")) {
|
if (isAllowedResource("configmaps")) {
|
||||||
routes.push({
|
routes.push({
|
||||||
title: <Trans>ConfigMaps</Trans>,
|
title: <Trans>ConfigMaps</Trans>,
|
||||||
component: ConfigMaps,
|
component: ConfigMaps,
|
||||||
url: configMapsURL({ query }),
|
url: configMapsURL({ query }),
|
||||||
path: configMapsRoute.path,
|
path: configMapsRoute.path,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (isAllowedResource("secrets")) {
|
if (isAllowedResource("secrets")) {
|
||||||
routes.push({
|
routes.push({
|
||||||
@ -35,7 +35,7 @@ export class Config extends React.Component {
|
|||||||
component: Secrets,
|
component: Secrets,
|
||||||
url: secretsURL({ query }),
|
url: secretsURL({ query }),
|
||||||
path: secretsRoute.path,
|
path: secretsRoute.path,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (isAllowedResource("resourcequotas")) {
|
if (isAllowedResource("resourcequotas")) {
|
||||||
routes.push({
|
routes.push({
|
||||||
@ -43,7 +43,7 @@ export class Config extends React.Component {
|
|||||||
component: ResourceQuotas,
|
component: ResourceQuotas,
|
||||||
url: resourceQuotaURL({ query }),
|
url: resourceQuotaURL({ query }),
|
||||||
path: resourceQuotaRoute.path,
|
path: resourceQuotaRoute.path,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (isAllowedResource("horizontalpodautoscalers")) {
|
if (isAllowedResource("horizontalpodautoscalers")) {
|
||||||
routes.push({
|
routes.push({
|
||||||
@ -51,12 +51,12 @@ export class Config extends React.Component {
|
|||||||
component: HorizontalPodAutoscalers,
|
component: HorizontalPodAutoscalers,
|
||||||
url: hpaURL({ query }),
|
url: hpaURL({ query }),
|
||||||
path: hpaRoute.path,
|
path: hpaRoute.path,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const tabRoutes = Config.tabRoutes;
|
const tabRoutes = Config.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Config" tabs={tabRoutes}>
|
<MainLayout className="Config" tabs={tabRoutes}>
|
||||||
@ -65,6 +65,6 @@ export class Config extends React.Component {
|
|||||||
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./config.route"
|
export * from "./config.route";
|
||||||
export * from "./config"
|
export * from "./config";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "./certificate-details.scss"
|
import "./certificate-details.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import moment from "moment"
|
import moment from "moment";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
@ -19,9 +19,11 @@ interface Props extends KubeObjectDetailsProps<Certificate> {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CertificateDetails extends React.Component<Props> {
|
export class CertificateDetails extends React.Component<Props> {
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { object: cert, className } = this.props;
|
const { object: cert, className } = this.props;
|
||||||
if (!cert) return;
|
if (!cert) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { spec, status } = cert;
|
const { spec, status } = cert;
|
||||||
const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec;
|
const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec;
|
||||||
const { lastFailureTime, notAfter } = status;
|
const { lastFailureTime, notAfter } = status;
|
||||||
@ -104,7 +106,7 @@ export class CertificateDetails extends React.Component<Props> {
|
|||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
className={cssNames({ [type.toLowerCase()]: isReady })}
|
className={cssNames({ [type.toLowerCase()]: isReady })}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
@ -126,7 +128,7 @@ export class CertificateDetails extends React.Component<Props> {
|
|||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -139,4 +141,4 @@ export class CertificateDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
apiManager.registerViews(certificatesApi, {
|
apiManager.registerViews(certificatesApi, {
|
||||||
Details: CertificateDetails
|
Details: CertificateDetails
|
||||||
})
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user