mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor dashboard server logic to electron main (#146)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
e54b79b56a
commit
1aaa695cfe
38 files changed
+430
-1323
No files matched your search
@@ -14,6 +14,7 @@ import { UserManagement } from "./+user-management/user-management";
|
||||
import { ConfirmDialog } from "./confirm-dialog";
|
||||
import { usersManagementRoute } from "./+user-management/user-management.routes";
|
||||
import { clusterRoute, clusterURL } from "./+cluster";
|
||||
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
|
||||
import { Nodes, nodesRoute } from "./+nodes";
|
||||
import { Workloads, workloadsRoute, workloadsURL } from "./+workloads";
|
||||
import { Namespaces, namespacesRoute } from "./+namespaces";
|
||||
@@ -70,6 +71,7 @@ class App extends React.Component {
|
||||
<KubeObjectDetails/>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
<KubeConfigDialog/>
|
||||
<AddRoleBindingDialog/>
|
||||
<PodLogsDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
|
||||
+4
-22
@@ -1,31 +1,15 @@
|
||||
{
|
||||
"name": "kontena-lens",
|
||||
"version": "1.13.1",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"app-dev": "yarn extract && webpack-cli --watch --cache --progress --output-path ../static/build/client/",
|
||||
"build-client": "yarn extract && NODE_ENV=production webpack -p --cache --progress --output-path ../static/build/client/",
|
||||
"build-server": "tsc --project server",
|
||||
"pkg-server-linux": "pkg --no-bytecode --public --public-packages '*' -t node12-linux -o ../binaries/server/linux/lens-server build/server/app.js",
|
||||
"pkg-server-macos": "pkg --no-bytecode --public --public-packages '*' -t node12-macos -o ../binaries/server/darwin/lens-server build/server/app.js",
|
||||
"pkg-server-win": "pkg --no-bytecode --public --public-packages '*' -t node12-win -o ../binaries/server/win32/lens-server-x64.exe build/server/app.js",
|
||||
"dev": "yarn extract && webpack-cli --watch --cache --progress --output-path ../static/build/client/",
|
||||
"build": "yarn extract && NODE_ENV=production webpack -p --cache --progress --output-path ../static/build/client/",
|
||||
"test": "jest --config './test/jest.config.js'",
|
||||
"add-locale": "lingui add-locale",
|
||||
"extract": "lingui extract --clean",
|
||||
"compile": "lingui compile"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
".env",
|
||||
"package.json",
|
||||
"server"
|
||||
],
|
||||
"ext": "ts",
|
||||
"execMap": {
|
||||
"ts": "ts-node --project server/tsconfig.json"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"axios": "^0.19.0",
|
||||
"chalk": "^2.4.2",
|
||||
"compression": "^1.7.4",
|
||||
@@ -33,9 +17,6 @@
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^3.21.2",
|
||||
"http-proxy-middleware": "^0.20.0",
|
||||
"ip": "^1.1.5",
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsonpath": "^1.0.2",
|
||||
@@ -76,6 +57,7 @@
|
||||
"@types/http-proxy-middleware": "^0.19.3",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/jest": "^24.0.22",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/lingui__macro": "^2.7.3",
|
||||
"@types/lodash": "^4.14.146",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Get certificate auth data
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import config from "../config";
|
||||
|
||||
let caData: string = null
|
||||
|
||||
export async function getCertificateAuthorityData(encoding = 'utf8'): Promise<string> {
|
||||
if (caData) {
|
||||
return caData
|
||||
}
|
||||
if (!fs.existsSync(config.KUBERNETES_CA_CERT)) {
|
||||
caData = config.KUBERNETES_CA_CERT
|
||||
return caData
|
||||
}
|
||||
try {
|
||||
const ca = await util.promisify(fs.readFile)(config.KUBERNETES_CA_CERT);
|
||||
return Buffer.from(ca).toString(encoding);
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Get cluster info
|
||||
|
||||
import config from "../config"
|
||||
import { kubeRequest } from "./kube-request";
|
||||
import { IClusterInfo, IClusterConfigMap } from "../common/cluster"
|
||||
|
||||
export async function getClusterInfo(): Promise<IClusterInfo> {
|
||||
const [configMap, kubeVersion, pharosVersion] = await Promise.all([
|
||||
getClusterConfigMap().catch(() => ({} as IClusterConfigMap)),
|
||||
getKubeVersion().catch(() => null),
|
||||
getPharosVersion().catch(() => null),
|
||||
]);
|
||||
return {
|
||||
...configMap,
|
||||
kubeVersion,
|
||||
pharosVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getClusterConfigMap() {
|
||||
const res = await kubeRequest<{ data: IClusterConfigMap }>({
|
||||
path: `/api/v1/namespaces/${config.LENS_NAMESPACE}/configmaps/config`,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getKubeVersion() {
|
||||
const res = await kubeRequest<{ gitVersion: string }>({
|
||||
path: "/version",
|
||||
});
|
||||
return res.gitVersion.slice(1);
|
||||
}
|
||||
|
||||
export async function getPharosVersion() {
|
||||
const res = await kubeRequest<{ data: { "pharos-version": string } }>({
|
||||
path: `/api/v1/namespaces/kube-system/configmaps/pharos-config`,
|
||||
});
|
||||
return res ? res.data["pharos-version"] : null;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Get namespaces
|
||||
|
||||
import config from "../config";
|
||||
import { KubeJsonApiDataList } from "../../client/api/kube-json-api";
|
||||
import { IKubeRequestParams, kubeRequest } from "./kube-request";
|
||||
import { reviewResourceAccess } from "./review-resource-access";
|
||||
import { getServiceAccountToken } from "./get-service-account-token"
|
||||
|
||||
export async function getNamespaces(params: Partial<IKubeRequestParams> = {}) {
|
||||
return kubeRequest<KubeJsonApiDataList>({
|
||||
...params,
|
||||
path: "/api/v1/namespaces",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllowedNamespaces(
|
||||
params: Partial<IKubeRequestParams>,
|
||||
fallbackNs = config.KUBERNETES_NAMESPACE,
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const allNamespaces = await getNamespaces(params);
|
||||
const nsAccessStatuses = await Promise.all(
|
||||
allNamespaces.items.map(ns => {
|
||||
const { name } = ns.metadata;
|
||||
return reviewResourceAccess(params, {
|
||||
namespace: name,
|
||||
resource: "pods",
|
||||
verb: "list",
|
||||
});
|
||||
})
|
||||
);
|
||||
return allNamespaces.items
|
||||
.filter((ns, i) => nsAccessStatuses[i].allowed)
|
||||
.map(ns => ns.metadata.name);
|
||||
} catch (e) {
|
||||
const serviceToken = await getServiceAccountToken();
|
||||
if (!serviceToken) {
|
||||
return fallbackNs ? [fallbackNs] : [];
|
||||
}
|
||||
// fetch namespaces with service-account token (cluster-wide)
|
||||
// and for every namespace make additional request to check if namespace available for user-token
|
||||
const allNamespaces = await getNamespaces({
|
||||
authHeader: `Bearer ${serviceToken}`
|
||||
});
|
||||
const nsAccessStatuses = await Promise.all(
|
||||
allNamespaces.items.map(ns => {
|
||||
const { name } = ns.metadata;
|
||||
return reviewResourceAccess(params, {
|
||||
namespace: name,
|
||||
resource: "pods",
|
||||
verb: "list",
|
||||
});
|
||||
})
|
||||
);
|
||||
return allNamespaces.items
|
||||
.filter((ns, i) => nsAccessStatuses[i].allowed)
|
||||
.map(ns => ns.metadata.name);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Get service-account token
|
||||
|
||||
import { existsSync, readFile } from "fs";
|
||||
import { promisify } from "util";
|
||||
import config from "../config"
|
||||
|
||||
const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
||||
|
||||
export async function getServiceAccountToken() {
|
||||
const { SERVICE_ACCOUNT_TOKEN } = config;
|
||||
|
||||
if (SERVICE_ACCOUNT_TOKEN) {
|
||||
return SERVICE_ACCOUNT_TOKEN;
|
||||
}
|
||||
|
||||
if (existsSync(tokenPath)) {
|
||||
const token = await promisify(readFile)(tokenPath);
|
||||
return token.toString().trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Check cluster-admin rights for auth-token
|
||||
// CLI: kubectl auth can-i '*' '*' --all-namespaces
|
||||
|
||||
import { reviewResourceAccess } from "./review-resource-access";
|
||||
import { IKubeRequestParams } from "./kube-request";
|
||||
|
||||
export async function isClusterAdmin(params: Partial<IKubeRequestParams>): Promise<boolean> {
|
||||
try {
|
||||
const accessCheck = await reviewResourceAccess(params, {
|
||||
resource: "*",
|
||||
namespace: "*",
|
||||
group: "*",
|
||||
verb: "*",
|
||||
});
|
||||
return accessCheck.allowed;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Kubernetes request api helper
|
||||
|
||||
import config, { isSecure } from "../config";
|
||||
import axios, { AxiosError, AxiosRequestConfig } from "axios"
|
||||
import * as https from "https";
|
||||
import { getCertificateAuthorityData } from "./get-cert-auth-data";
|
||||
import { logger, sanitizeHeaders } from "../utils/logger";
|
||||
import { getServiceAccountToken } from "./get-service-account-token";
|
||||
|
||||
export interface IKubeRequestParams extends AxiosRequestConfig {
|
||||
path: string;
|
||||
authHeader?: string;
|
||||
}
|
||||
|
||||
export async function kubeRequest<T>(params: IKubeRequestParams): Promise<T> {
|
||||
const { KUBE_CLUSTER_URL, KUBERNETES_CLIENT_CERT, KUBERNETES_CLIENT_KEY } = config;
|
||||
const serviceToken = await getServiceAccountToken();
|
||||
const defaultAuthHeader = serviceToken ? `Bearer ${serviceToken}` : "";
|
||||
const {
|
||||
authHeader = defaultAuthHeader,
|
||||
url = KUBE_CLUSTER_URL,
|
||||
path = "",
|
||||
...reqConfig
|
||||
} = params;
|
||||
|
||||
// add access token
|
||||
reqConfig.headers = Object.assign({}, reqConfig.headers, {
|
||||
"Content-type": "application/json",
|
||||
});
|
||||
|
||||
if (!KUBERNETES_CLIENT_CERT && authHeader) {
|
||||
reqConfig.headers["Authorization"] = authHeader;
|
||||
}
|
||||
|
||||
// allow requests to kube-cluster without valid ssl certs..
|
||||
reqConfig.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: isSecure(),
|
||||
cert: KUBERNETES_CLIENT_CERT,
|
||||
key: KUBERNETES_CLIENT_KEY,
|
||||
ca: await getCertificateAuthorityData(),
|
||||
});
|
||||
|
||||
const reqUrl = url + path;
|
||||
|
||||
return axios(reqUrl, reqConfig)
|
||||
.then(res => res.data)
|
||||
.catch((error: AxiosError<T>) => {
|
||||
const { message, config } = error;
|
||||
logger.error(`[KUBE-REQUEST]: ${message}`, {
|
||||
code: error.code,
|
||||
method: config.method,
|
||||
url: config.url,
|
||||
headers: sanitizeHeaders(config.headers),
|
||||
params: config.params,
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Get resource access review
|
||||
// Docs: https://kubernetes.io/docs/reference/access-authn-authz/authorization/
|
||||
import { IKubeRequestParams, kubeRequest } from "./kube-request";
|
||||
|
||||
interface IResourceAccess {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
status: IResourceAccessStatus;
|
||||
}
|
||||
|
||||
export interface IResourceAccessStatus {
|
||||
allowed: boolean;
|
||||
denied?: boolean;
|
||||
reason?: string;
|
||||
evaluationError?: string;
|
||||
}
|
||||
|
||||
interface IResourceAccessAttributes {
|
||||
group?: string | "*";
|
||||
resource?: string | "*";
|
||||
verb?: "get" | "list" | "create" | "update" | "patch" | "watch" | "proxy" | "redirect" | "delete" | "deletecollection" | "*";
|
||||
namespace?: string | "*";
|
||||
}
|
||||
|
||||
export async function reviewResourceAccess(
|
||||
params: Partial<IKubeRequestParams> = {},
|
||||
attrs: IResourceAccessAttributes
|
||||
): Promise<IResourceAccessStatus> {
|
||||
try {
|
||||
const accessReview = await kubeRequest<IResourceAccess>({
|
||||
...params,
|
||||
method: "POST",
|
||||
path: "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews",
|
||||
data: {
|
||||
spec: {
|
||||
resourceAttributes: attrs
|
||||
}
|
||||
}
|
||||
});
|
||||
return accessReview.status;
|
||||
} catch (err) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: err.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Check validity of auth-token
|
||||
import { kubeRequest } from "./kube-request";
|
||||
|
||||
export interface ITokenReview {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
status: ITokenReviewStatus;
|
||||
}
|
||||
|
||||
export interface ITokenReviewStatus {
|
||||
authenticated: boolean;
|
||||
user: {
|
||||
username?: string;
|
||||
uid?: string;
|
||||
groups?: string[];
|
||||
};
|
||||
error?: string[];
|
||||
}
|
||||
|
||||
export async function reviewToken(authToken: string): Promise<ITokenReviewStatus> {
|
||||
try {
|
||||
const tokenReview = await kubeRequest<ITokenReview>({
|
||||
path: "/apis/authentication.k8s.io/v1/tokenreviews",
|
||||
method: "POST",
|
||||
data: {
|
||||
spec: {
|
||||
token: authToken
|
||||
}
|
||||
}
|
||||
});
|
||||
return tokenReview.status;
|
||||
} catch (err) {
|
||||
return {
|
||||
authenticated: false,
|
||||
user: {},
|
||||
error: [err.toString()],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import config, { BUILD_DIR, CLIENT_DIR } from "../server/config"
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import express from "express"
|
||||
import cookieSession from "cookie-session"
|
||||
import compression from "compression"
|
||||
import helmet from "helmet"
|
||||
import morgan from "morgan"
|
||||
import { logger } from "../server/utils/logger"
|
||||
import { configRoute, kubeconfigRoute, kubewatchRoute, metricsRoute, readyStateRoute } from "../server/routes";
|
||||
import { useRequestHeaderToken } from "../server/middlewares";
|
||||
|
||||
const {
|
||||
IS_PRODUCTION, LOCAL_SERVER_PORT, API_PREFIX,
|
||||
SESSION_NAME, SESSION_SECRET,
|
||||
} = config;
|
||||
|
||||
const app = express();
|
||||
const localApis = express.Router();
|
||||
const outputDir = path.resolve(process.cwd(), BUILD_DIR, CLIENT_DIR);
|
||||
|
||||
app.set('trust proxy', 1); // trust first proxy
|
||||
|
||||
localApis.use(
|
||||
configRoute(),
|
||||
readyStateRoute(),
|
||||
kubeconfigRoute(),
|
||||
kubewatchRoute(),
|
||||
metricsRoute()
|
||||
);
|
||||
|
||||
// https://github.com/expressjs/cookie-session
|
||||
app.use(cookieSession({
|
||||
name: SESSION_NAME,
|
||||
secret: SESSION_SECRET,
|
||||
secure: IS_PRODUCTION,
|
||||
httpOnly: true,
|
||||
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
|
||||
}));
|
||||
|
||||
// protect from well-known web vulnerabilities by setting HTTP headers appropriately
|
||||
// https://github.com/helmetjs/helmet
|
||||
app.use(helmet({
|
||||
hsts: {
|
||||
includeSubDomains: false,
|
||||
}
|
||||
}));
|
||||
|
||||
// use auth-token from request headers (if applicable via proxy)
|
||||
app.use(useRequestHeaderToken());
|
||||
|
||||
// requests logging
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
// enable gzip compression
|
||||
app.use(compression());
|
||||
|
||||
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
app.use("/", express.static(outputDir)); // handle static files (assets)
|
||||
|
||||
app.use(API_PREFIX.BASE, express.json({ limit: "10mb" }), localApis);
|
||||
|
||||
// handle all page requests via index.html, in development mode it's managed by webpack-dev-server
|
||||
app.all('*', (req, res) => {
|
||||
const indexHtml = path.resolve(outputDir, 'index.html');
|
||||
if (fs.existsSync(indexHtml)) res.sendFile(indexHtml);
|
||||
else {
|
||||
res.send("Error: build/index.html doesn't exists");
|
||||
}
|
||||
});
|
||||
|
||||
// run server
|
||||
const server = app.listen(LOCAL_SERVER_PORT, "127.0.0.1", () => {
|
||||
logger.appStarted(LOCAL_SERVER_PORT, 'Server started');
|
||||
});
|
||||
@@ -1,9 +1,4 @@
|
||||
export interface IClusterConfigMap {
|
||||
clusterName: string;
|
||||
clusterUrl: string;
|
||||
}
|
||||
|
||||
export interface IClusterInfo extends IClusterConfigMap {
|
||||
export interface IClusterInfo {
|
||||
kubeVersion?: string;
|
||||
pharosVersion?: string;
|
||||
clusterName?: string;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./kube-proxy"
|
||||
export * from "./terminal-proxy"
|
||||
export * from "./use-header-token"
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Request } from "express";
|
||||
import proxy from "http-proxy-middleware"
|
||||
import { userSession } from "../user-session";
|
||||
import config, { isSecure } from "../config";
|
||||
|
||||
export function kubeProxy(serviceUrl: string, proxyConfig: proxy.Config = {}) {
|
||||
const { IS_PRODUCTION } = config;
|
||||
return proxy({
|
||||
target: serviceUrl,
|
||||
secure: isSecure(), // verify the ssl certs
|
||||
logLevel: IS_PRODUCTION ? "info" : "debug",
|
||||
changeOrigin: true, // needed for virtual hosted sites
|
||||
pathRewrite: (path, req: Request) => {
|
||||
return path.replace(req.baseUrl, ""); // remove client-prefix, e.g "/api-kube"
|
||||
},
|
||||
onProxyReq(proxyReq, req: Request, res) {
|
||||
const { authHeader } = userSession.get(req);
|
||||
if (authHeader) {
|
||||
proxyReq.setHeader("Authorization", authHeader);
|
||||
}
|
||||
},
|
||||
...proxyConfig,
|
||||
})
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NextFunction } from "express";
|
||||
import proxy from "http-proxy-middleware"
|
||||
import appConfig from "../config"
|
||||
|
||||
const { KUBE_TERMINAL_URL, API_PREFIX, IS_PRODUCTION } = appConfig;
|
||||
|
||||
interface ITerminalProxy extends NextFunction {
|
||||
upgrade: () => void;
|
||||
}
|
||||
|
||||
export const terminalProxy = proxy({
|
||||
target: KUBE_TERMINAL_URL,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
logLevel: IS_PRODUCTION ? "info" : "debug",
|
||||
pathRewrite: {
|
||||
["^" + API_PREFIX.TERMINAL]: "" // remove api-prefix
|
||||
}
|
||||
}) as ITerminalProxy;
|
||||
@@ -1,20 +0,0 @@
|
||||
// Allow to use "Authorization" from request for auto-login (when provided by proxy)
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
import { userSession } from "../user-session";
|
||||
|
||||
export function useRequestHeaderToken() {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const authorization = req.headers["authorization"] || req.headers["x-lens-kubectl-token"];
|
||||
const { authHeader, isUserLogin } = userSession.get(req);
|
||||
const userHasOwnToken = authHeader && isUserLogin;
|
||||
|
||||
// don't overwrite user's login credentials
|
||||
if (authorization && !userHasOwnToken && authHeader !== authorization) {
|
||||
userSession.save(req, {
|
||||
authHeader: authorization.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//-- Config route
|
||||
|
||||
import config from "../config";
|
||||
import { IConfig } from "../common/config"
|
||||
import { Router } from "express";
|
||||
import { userSession } from "../user-session";
|
||||
import { getClusterInfo } from "../api/get-cluster-info";
|
||||
import { isClusterAdmin } from "../api/is-cluster-admin";
|
||||
import { getAllowedNamespaces } from "../api/get-namespaces";
|
||||
import { parseJwt } from "../utils/parse-jwt";
|
||||
|
||||
export function configRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/config')
|
||||
.get(async (req, res) => {
|
||||
const { username, authHeader } = userSession.get(req);
|
||||
const authToken = userSession.getToken(req);
|
||||
|
||||
const data: IConfig = {
|
||||
clusterName: config.KUBE_CLUSTER_NAME,
|
||||
lensVersion: config.LENS_VERSION,
|
||||
lensTheme: config.LENS_THEME,
|
||||
chartsEnabled: !!config.CHARTS_ENABLED,
|
||||
kubectlAccess: !!req.headers["x-lens-kubectl-token"]
|
||||
};
|
||||
|
||||
// load config data from other places
|
||||
const loading: Promise<any>[] = [
|
||||
getClusterInfo().then(info => Object.assign(data, info)),
|
||||
];
|
||||
|
||||
// validate user token from session and fetch more config data
|
||||
if (authToken) {
|
||||
const { sub, email } = parseJwt(authToken);
|
||||
data.username = email || sub || username;
|
||||
data.token = authToken;
|
||||
loading.push(
|
||||
isClusterAdmin({ authHeader }).then(isAdmin => data.isClusterAdmin = isAdmin),
|
||||
getAllowedNamespaces({ authHeader }).then(list => data.allowedNamespaces = list),
|
||||
);
|
||||
}
|
||||
await Promise.all(loading);
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from "./config-route"
|
||||
export * from "./kubeconfig-route"
|
||||
export * from "./kubewatch-route"
|
||||
export * from "./metrics-route"
|
||||
export * from "./ready-state-route"
|
||||
@@ -1,100 +0,0 @@
|
||||
//-- Kubeconfig route
|
||||
|
||||
import { Router } from "express";
|
||||
import { AxiosError } from "axios";
|
||||
import { userSession } from "../user-session";
|
||||
import { kubeRequest } from "../api/kube-request";
|
||||
import { Secret } from "../../client/api/endpoints";
|
||||
import { base64 } from "../../client/utils/base64";
|
||||
import { getCertificateAuthorityData } from "../api/get-cert-auth-data";
|
||||
import { getClusterConfigMap } from "../api/get-cluster-info";
|
||||
|
||||
interface IKubeConfigParams {
|
||||
username: string;
|
||||
authToken: string;
|
||||
certificateAuthority: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export function kubeconfigRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/kubeconfig/user')
|
||||
.get(async (req, res) => {
|
||||
const { username = "kubernetes" } = userSession.get(req);
|
||||
const authToken = userSession.getToken(req);
|
||||
const cert = await getCertificateAuthorityData('base64');
|
||||
const data = await generateKubeConfig({
|
||||
username,
|
||||
authToken: authToken,
|
||||
certificateAuthority: cert
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
router.route('/kubeconfig/service-account/:namespace/:account')
|
||||
.get(async (req, res) => {
|
||||
const { authHeader } = userSession.get(req);
|
||||
const { namespace, account } = req.params;
|
||||
try {
|
||||
const secret = await kubeRequest<{ items: Secret[] }>({
|
||||
path: `/api/v1/namespaces/${namespace}/secrets`,
|
||||
authHeader: authHeader,
|
||||
}).then(secrets => {
|
||||
return secrets.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
return annotations && annotations["kubernetes.io/service-account.name"] == account;
|
||||
});
|
||||
});
|
||||
const data = await generateKubeConfig({
|
||||
username: account,
|
||||
namespace: namespace,
|
||||
authToken: base64.decode(secret.data.token),
|
||||
certificateAuthority: secret.data["ca.crt"],
|
||||
});
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
const { response }: AxiosError = err;
|
||||
res.status(403).json(response ? response.data : err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function generateKubeConfig(params: IKubeConfigParams) {
|
||||
const { clusterName, clusterUrl } = await getClusterConfigMap();
|
||||
const { authToken, username, certificateAuthority, namespace = "" } = params;
|
||||
return {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Config',
|
||||
'clusters': [
|
||||
{
|
||||
'name': clusterName,
|
||||
'cluster': {
|
||||
'server': clusterUrl,
|
||||
'certificate-authority-data': certificateAuthority
|
||||
}
|
||||
}
|
||||
],
|
||||
'users': [
|
||||
{
|
||||
'name': username,
|
||||
'user': {
|
||||
'token': authToken,
|
||||
}
|
||||
}
|
||||
],
|
||||
'contexts': [
|
||||
{
|
||||
'name': clusterName,
|
||||
'context': {
|
||||
'user': username,
|
||||
'cluster': clusterName,
|
||||
'namespace': namespace,
|
||||
}
|
||||
}
|
||||
],
|
||||
'current-context': clusterName
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
//-- Streaming k8s watch-api events
|
||||
|
||||
import axios from "axios"
|
||||
import { Router } from "express";
|
||||
import { IncomingMessage } from "http";
|
||||
import { kubeRequest } from "../api/kube-request";
|
||||
import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery} from "../common/kubewatch"
|
||||
import { userSession } from "../user-session";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export function kubewatchRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/watch')
|
||||
.get(async (req, res) => {
|
||||
const { authHeader } = userSession.get(req);
|
||||
const queryParams: IKubeWatchRouteQuery = req.query;
|
||||
const apis: string[] = [].concat(queryParams.api || []);
|
||||
const streams = new Map<string, IncomingMessage>();
|
||||
const eventsBuffer = new Map<string, IKubeWatchEvent>();
|
||||
let isClosing = false;
|
||||
|
||||
if (!apis.length) {
|
||||
res.status(400).json({
|
||||
message: "Empty request. Query params 'api' are not provided.",
|
||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.header({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
|
||||
// init streams
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
apis.forEach(apiUrl => {
|
||||
console.log("[KUBE-WATCH] init stream", apiUrl);
|
||||
const connecting = kubeRequest<IncomingMessage>({
|
||||
path: apiUrl,
|
||||
responseType: "stream",
|
||||
authHeader: authHeader,
|
||||
cancelToken: cancelToken.token,
|
||||
});
|
||||
connecting.then(stream => {
|
||||
streams.set(apiUrl, stream); // save connection for clean up
|
||||
stream.socket.setKeepAlive(true); // keep connection alive
|
||||
let lastUnusedBuffer = ""
|
||||
return stream
|
||||
.on("data", (buffer: Buffer) => {
|
||||
const data = lastUnusedBuffer + buffer.toString().trim();
|
||||
data.split("\n").map(str => {
|
||||
try {
|
||||
const eventObj = JSON.parse(str);
|
||||
bufferEvent(eventObj); // handle
|
||||
lastUnusedBuffer = ""; // clean up since parsing was successful
|
||||
} catch (err) {
|
||||
lastUnusedBuffer = str; // invalid json, tail must wait next incoming data
|
||||
}
|
||||
});
|
||||
})
|
||||
.on("end", () => {
|
||||
// client must update resource-version and try to reconnect
|
||||
console.log(`[KUBE-WATCH] stream ended ${apiUrl}`)
|
||||
sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: apiUrl,
|
||||
status: stream.statusCode,
|
||||
})
|
||||
});
|
||||
}, err => {
|
||||
logger.error(`[KUBE-WATCH] error ${apiUrl}`, err);
|
||||
sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: apiUrl,
|
||||
status: 410,
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
function getEventBufferId(evt: IKubeWatchEvent) {
|
||||
const { object, type } = evt;
|
||||
const { kind } = object;
|
||||
let { metadata: { uid } } = object;
|
||||
if (kind === "Event") {
|
||||
uid = (object as any).involvedObject.uid; // reason: uid for events always unique
|
||||
}
|
||||
return `${type}:${kind}-${uid}`
|
||||
}
|
||||
|
||||
function bufferEvent(evt: IKubeWatchEvent) {
|
||||
const id = getEventBufferId(evt);
|
||||
if (eventsBuffer.has(id)) {
|
||||
eventsBuffer.delete(id); // clear to move event to the end in map's "timeline"
|
||||
}
|
||||
eventsBuffer.set(id, evt); // save latest event by object's identity
|
||||
}
|
||||
|
||||
function sendEvent(evt: IKubeWatchEvent | IKubeWatchRouteEvent, autoFlush = true) {
|
||||
if (isClosing) return;
|
||||
// convert to "text/event-stream" format
|
||||
res.write(`data: ${JSON.stringify(evt)}\n\n`);
|
||||
if (autoFlush) {
|
||||
// @ts-ignore
|
||||
res.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// process sending events
|
||||
const flushInterval = setInterval(() => {
|
||||
const eventsPack = Array.from(eventsBuffer.entries())
|
||||
.slice(0, 100) // max limit per sending
|
||||
.map(([id, evt]) => {
|
||||
eventsBuffer.delete(id); // clean up used event
|
||||
return evt;
|
||||
});
|
||||
if (eventsPack.length > 0) {
|
||||
eventsPack.forEach(evt => sendEvent(evt, false));
|
||||
// @ts-ignore
|
||||
res.flush();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
function onClose() {
|
||||
if (isClosing) return;
|
||||
isClosing = true;
|
||||
clearInterval(flushInterval);
|
||||
streams.forEach(stream => stream.removeAllListeners("end"));
|
||||
cancelToken.cancel();
|
||||
}
|
||||
|
||||
req.on("close", onClose);
|
||||
res.on("finish", onClose);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//-- Metrics
|
||||
// https://prometheus.io/docs/prometheus/latest/querying/api/
|
||||
|
||||
import { Router } from "express";
|
||||
import config from "../config";
|
||||
import { kubeRequest } from "../api/kube-request";
|
||||
import { userSession } from "../user-session";
|
||||
import { AxiosError } from "axios";
|
||||
import { IMetrics } from "../../client/api/endpoints/metrics.api";
|
||||
import { IMetricsQuery } from "../common/metrics"
|
||||
|
||||
|
||||
export function metricsRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.post("/metrics", async (req, res, next) => {
|
||||
const { authHeader } = userSession.get(req);
|
||||
const { namespace, ...queryParams } = req.query;
|
||||
const query: IMetricsQuery = req.body;
|
||||
|
||||
/*eslint-disable */
|
||||
// add default namespace for rbac-proxy validation
|
||||
if (!queryParams.kubernetes_namespace) {
|
||||
queryParams.kubernetes_namespace = config.STATS_NAMESPACE;
|
||||
}
|
||||
/*eslint-enble */
|
||||
|
||||
// prometheus metrics loader
|
||||
const attempts: { [query: string]: number } = {};
|
||||
const maxAttempts = 5;
|
||||
const loadMetrics = (query: string): Promise<IMetrics> => {
|
||||
const attempt = attempts[query] = (attempts[query] || 0) + 1;
|
||||
return kubeRequest<IMetrics>({
|
||||
url: config.KUBE_METRICS_URL,
|
||||
path: "/api/v1/query_range",
|
||||
authHeader: authHeader,
|
||||
params: {
|
||||
query: query,
|
||||
...queryParams,
|
||||
},
|
||||
}).catch(async (err: AxiosError) => {
|
||||
// https://github.com/axios/axios#handling-errors
|
||||
if (!err.response && attempt < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
|
||||
return loadMetrics(query);
|
||||
}
|
||||
return {
|
||||
status: err.toString(),
|
||||
data: {
|
||||
result: []
|
||||
},
|
||||
} as IMetrics;
|
||||
})
|
||||
};
|
||||
|
||||
// return data in same structure as query
|
||||
let data: any;
|
||||
try {
|
||||
if (typeof query === "string") {
|
||||
data = await loadMetrics(query)
|
||||
}
|
||||
else if (Array.isArray(query)) {
|
||||
data = await Promise.all(query.map(loadMetrics));
|
||||
}
|
||||
else {
|
||||
data = {};
|
||||
const result = await Promise.all(
|
||||
Object.values(query).map(loadMetrics)
|
||||
);
|
||||
Object.keys(query).forEach((metricName, index) => {
|
||||
data[metricName] = result[index];
|
||||
});
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//-- App readiness checker
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
export function readyStateRoute() {
|
||||
const router = Router();
|
||||
|
||||
router.route('/ready')
|
||||
.get(async (req, res) => {
|
||||
const serviceWaitingList: string[] = [];
|
||||
|
||||
res.json(serviceWaitingList);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//-- User sessions helper
|
||||
|
||||
import { Request } from "express";
|
||||
import CookieSessionObject = CookieSessionInterfaces.CookieSessionObject;
|
||||
|
||||
interface IUserSession extends CookieSessionObject {
|
||||
authHeader: string;
|
||||
username?: string;
|
||||
isUserLogin?: boolean; // authorization via user's manual login with credentials
|
||||
}
|
||||
|
||||
export const userSession = {
|
||||
get(req: Request): Partial<IUserSession> {
|
||||
return req.session;
|
||||
},
|
||||
save(req: Request, data: Partial<IUserSession> = {}) {
|
||||
Object.assign(req.session, data);
|
||||
},
|
||||
getToken(req: Request): string {
|
||||
const { authHeader = "" } = this.get(req);
|
||||
const [type, token = ""] = authHeader.split(" ");
|
||||
return token;
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
// Load & parse local kubernetes config (dev-only)
|
||||
|
||||
import * as jsYaml from "js-yaml"
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import chalk from "chalk";
|
||||
import { logger } from "./logger";
|
||||
|
||||
interface IKubeConfigParams {
|
||||
clusterUrl: string;
|
||||
userToken: string;
|
||||
}
|
||||
|
||||
export function getKubeConfigDev(): Partial<IKubeConfigParams> {
|
||||
const KUBE_CONFIG_FILE = process.env.KUBE_CONFIG_FILE;
|
||||
if (!KUBE_CONFIG_FILE) {
|
||||
return {}
|
||||
}
|
||||
let filePath = ""
|
||||
try {
|
||||
filePath = KUBE_CONFIG_FILE.replace("~", os.homedir());
|
||||
const yaml = fs.readFileSync(filePath).toString();
|
||||
const config = jsYaml.safeLoad(yaml);
|
||||
return {
|
||||
clusterUrl: config.clusters[0].cluster.server,
|
||||
userToken: config.users[0].user.token,
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[KUBE-CONFIG] Parsing config file ${chalk.bold(filePath)} failed.`, err)
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import * as ip from "ip"
|
||||
|
||||
const divider = chalk.gray('-----------------------------------');
|
||||
|
||||
export const logger = {
|
||||
// Called when express.js app starts on given port w/o errors
|
||||
appStarted: (port: string | number, title = 'Server started ') => {
|
||||
console.log(chalk.underline.bold(title) + ` ${chalk.green('✓')}`);
|
||||
console.log(`
|
||||
${chalk.bold('Access URLs:')}
|
||||
${divider}
|
||||
Localhost: ${chalk.magenta(`http://localhost:${port}`)}
|
||||
LAN: ${chalk.magenta(`http://${ip.address()}:${port}`)}
|
||||
${divider}
|
||||
`);
|
||||
},
|
||||
|
||||
error(message: string, error: any) {
|
||||
let errString = ""
|
||||
try {
|
||||
errString = JSON.stringify(error, null, 2);
|
||||
} catch (e) {
|
||||
errString = String(error);
|
||||
}
|
||||
console.error(chalk.bold.red(`[ERROR] -> ${message}`), errString);
|
||||
}
|
||||
};
|
||||
|
||||
export function sanitizeHeaders(headers: { [name: string]: string }) {
|
||||
if (headers.Authorization) {
|
||||
const [authType, authToken] = headers.Authorization.split(" ");
|
||||
headers.Authorization = `${authType} *****`
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Parse payload from jwt token
|
||||
// Format: https://github.com/kontena/kube-oidc#openid-connect-and-kubernetes
|
||||
import { base64 } from "../../client/utils/base64";
|
||||
|
||||
interface JwtPayload {
|
||||
"azp": string;// "1077841816959-kkdh0lvq1au80qv4gtubotvgs9am4a95.apps.googleusercontent.com",
|
||||
"aud": string;// "1077841816959-kkdh0lvq1au80qv4gtubotvgs9am4a95.apps.googleusercontent.com",
|
||||
"sub": string;// "103613003764490648449",
|
||||
"hd": string;// "redhat.com",
|
||||
"email": string;// "echiang@redhat.com",
|
||||
"email_verified": boolean; // true,
|
||||
"at_hash": string;// "OGDOjIJ92FkatDBoCm8ydg",
|
||||
"exp": number;// 1527203940,
|
||||
"iss": string;// "https://accounts.google.com",
|
||||
"iat": number;// 1527200340,
|
||||
"name": string; // "Eric Chiang",
|
||||
"picture": string; // "https://lh5.googleusercontent.com/-Cs2iHTXiETs/AAAAAAAAAAI/AAAAAAAAACM/0Q85UhZizjg/s96-c/photo.jpg",
|
||||
"given_name": string; // "Eric",
|
||||
"family_name": string; //"Chiang",
|
||||
"locale": string; // "en"
|
||||
}
|
||||
|
||||
export function parseJwt(token: string): Partial<JwtPayload> {
|
||||
try {
|
||||
const [header, payload, signature] = token.split(".");
|
||||
return base64.decode(payload);
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -2631,11 +2631,6 @@ boolbase@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
bowser@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.7.0.tgz#96eab1fa07fab08c1ec4c75977a7c8ddf8e0fe1f"
|
||||
integrity sha512-aIlMvstvu8x+34KEiOHD3AsBgdrzg6sxALYiukOWhFvGMbQI6TRP/iY0LMhUrHs56aD6P1G0Z7h45PUJaa5m9w==
|
||||
|
||||
boxen@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||
@@ -2919,11 +2914,6 @@ camelcase@^5.0.0, camelcase@^5.3.1:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
|
||||
camelize@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
|
||||
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
|
||||
|
||||
caniuse-lite@^1.0.30001030:
|
||||
version "1.0.30001033"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001033.tgz#60c328fb56860de60f9a2cb419c31fb80587cba0"
|
||||
@@ -3349,11 +3339,6 @@ content-disposition@0.5.3:
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
content-security-policy-builder@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz#0a2364d769a3d7014eec79ff7699804deb8cfcbb"
|
||||
integrity sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==
|
||||
|
||||
content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
@@ -3637,11 +3622,6 @@ dashdash@^1.12.0:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
dasherize@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308"
|
||||
integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=
|
||||
|
||||
data-urls@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
|
||||
@@ -3788,11 +3768,6 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
depd@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
@@ -3870,11 +3845,6 @@ dns-packet@^1.3.1:
|
||||
ip "^1.1.0"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
dns-prefetch-control@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz#73988161841f3dcc81f47686d539a2c702c88624"
|
||||
integrity sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==
|
||||
|
||||
dns-txt@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
|
||||
@@ -3970,11 +3940,6 @@ domutils@^1.5.1:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dont-sniff-mimetype@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz#c7d0427f8bcb095762751252af59d148b0a623b2"
|
||||
integrity sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==
|
||||
|
||||
dot-prop@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
|
||||
@@ -4354,11 +4319,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
|
||||
dependencies:
|
||||
homedir-polyfill "^1.0.1"
|
||||
|
||||
expect-ct@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62"
|
||||
integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g==
|
||||
|
||||
expect@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
|
||||
@@ -4525,11 +4485,6 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "^2.0.0"
|
||||
|
||||
feature-policy@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069"
|
||||
integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==
|
||||
|
||||
figgy-pudding@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
|
||||
@@ -4682,11 +4637,6 @@ fragment-cache@^0.2.1:
|
||||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
frameguard@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.1.0.tgz#bd1442cca1d67dc346a6751559b6d04502103a22"
|
||||
integrity sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
@@ -5096,47 +5046,6 @@ he@1.2.x:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
helmet-crossdomain@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e"
|
||||
integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==
|
||||
|
||||
helmet-csp@2.9.4:
|
||||
version "2.9.4"
|
||||
resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.4.tgz#801382bac98f2f88706dc5c89d95c7e31af3a4a9"
|
||||
integrity sha512-qUgGx8+yk7Xl8XFEGI4MFu1oNmulxhQVTlV8HP8tV3tpfslCs30OZz/9uQqsWPvDISiu/NwrrCowsZBhFADYqg==
|
||||
dependencies:
|
||||
bowser "^2.7.0"
|
||||
camelize "1.0.0"
|
||||
content-security-policy-builder "2.1.0"
|
||||
dasherize "2.0.0"
|
||||
|
||||
helmet@^3.21.2:
|
||||
version "3.21.2"
|
||||
resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.2.tgz#7e2a19d5f6d898a77b5d2858e8e4bb2cda59f19f"
|
||||
integrity sha512-okUo+MeWgg00cKB8Csblu8EXgcIoDyb5ZS/3u0W4spCimeVuCUvVZ6Vj3O2VJ1Sxpyb8jCDvzu0L1KKT11pkIg==
|
||||
dependencies:
|
||||
depd "2.0.0"
|
||||
dns-prefetch-control "0.2.0"
|
||||
dont-sniff-mimetype "1.1.0"
|
||||
expect-ct "0.2.0"
|
||||
feature-policy "0.3.0"
|
||||
frameguard "3.1.0"
|
||||
helmet-crossdomain "0.4.0"
|
||||
helmet-csp "2.9.4"
|
||||
hide-powered-by "1.1.0"
|
||||
hpkp "2.0.0"
|
||||
hsts "2.2.0"
|
||||
ienoopen "1.1.0"
|
||||
nocache "2.1.0"
|
||||
referrer-policy "1.2.0"
|
||||
x-xss-protection "1.3.0"
|
||||
|
||||
hide-powered-by@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a"
|
||||
integrity sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==
|
||||
|
||||
history@^4.10.1, history@^4.9.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
@@ -5187,18 +5096,6 @@ hpack.js@^2.1.6:
|
||||
readable-stream "^2.0.1"
|
||||
wbuf "^1.1.0"
|
||||
|
||||
hpkp@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672"
|
||||
integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=
|
||||
|
||||
hsts@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964"
|
||||
integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==
|
||||
dependencies:
|
||||
depd "2.0.0"
|
||||
|
||||
html-element-map@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c"
|
||||
@@ -5308,16 +5205,6 @@ http-proxy-middleware@0.19.1:
|
||||
lodash "^4.17.11"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
http-proxy-middleware@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.20.0.tgz#5b128f7207985c4ea91b53fab8ad897a48c690d6"
|
||||
integrity sha512-dNJAk71nEJhPiAczQH9hGvE/MT9kEs+zn2Dh+Hi94PGZe1GluQirC7mw5rdREUtWx6qGS1Gu0bZd4qEAg+REgw==
|
||||
dependencies:
|
||||
http-proxy "^1.17.0"
|
||||
is-glob "^4.0.1"
|
||||
lodash "^4.17.14"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
http-proxy@^1.17.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
|
||||
@@ -5372,11 +5259,6 @@ ieee754@^1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||
|
||||
ienoopen@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974"
|
||||
integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ==
|
||||
|
||||
iferr@^0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||
@@ -7231,11 +7113,6 @@ no-case@^2.2.0:
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
nocache@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
|
||||
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
|
||||
|
||||
node-forge@0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
@@ -8568,11 +8445,6 @@ redent@^1.0.0:
|
||||
indent-string "^2.1.0"
|
||||
strip-indent "^1.0.1"
|
||||
|
||||
referrer-policy@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e"
|
||||
integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==
|
||||
|
||||
reflect.ownkeys@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||
@@ -10503,11 +10375,6 @@ ws@^6.2.1:
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
x-xss-protection@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c"
|
||||
integrity sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==
|
||||
|
||||
xdg-basedir@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
|
||||
|
||||
Reference in new issue
Block a user