@jfvilas/plugin-kwirth-backend
Version:
Backstage backend plugin for Kwirth plugins
245 lines (239 loc) • 11.3 kB
JavaScript
;
var express = require('express');
var Router = require('express-promise-router');
var catalogClient = require('@backstage/catalog-client');
var pluginKwirthCommon = require('@jfvilas/plugin-kwirth-common');
var config = require('./config.cjs.js');
var KwirthStaticData = require('../model/KwirthStaticData.cjs.js');
var permissions = require('./permissions.cjs.js');
var kwirthCommon = require('@jfvilas/kwirth-common');
var version = require('../version.cjs.js');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
async function createRouter(options) {
const { configSvc, loggerSvc, userInfoSvc, authSvc, httpAuthSvc, discoverySvc } = options;
loggerSvc.info("Loading static config");
if (!configSvc.has("kubernetes.clusterLocatorMethods")) {
loggerSvc.error(`Kwirth will not start, there is no 'clusterLocatorMethods' defined in app-config.`);
throw new Error("Kwirth backend will not be available.");
}
try {
config.loadClusters(loggerSvc, configSvc);
config.loadKwirthInfo(loggerSvc);
} catch (err) {
let txt = `Errors detected reading static configuration: ${err}`;
loggerSvc.error(txt);
throw new Error(txt);
}
if (configSvc.subscribe) {
configSvc.subscribe(() => {
try {
loggerSvc.warn("Change detected on app-config, Kwirth will update config.");
config.loadClusters(loggerSvc, configSvc);
} catch (err) {
loggerSvc.error(`Errors detected reading new configuration: ${err}`);
}
});
} else {
loggerSvc.info("Kwirth cannot subscribe to config changes.");
}
const router = Router__default.default();
router.use(express__default.default.json());
const createAuthFetchApi = (token) => {
return {
fetch: async (input, init) => {
init = init || {};
init.headers = {
...init.headers,
Authorization: `Bearer ${token}`
};
return fetch(input, init);
}
};
};
const getValidClustersFromEntity = async (entity) => {
let clusterList = [];
for (const clusterName of KwirthStaticData.KwirthStaticData.clusterKwirthData.keys()) {
let url = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(clusterName)?.kwirthHome;
let apiKeyStr = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(clusterName)?.kwirthApiKey;
let title = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(clusterName)?.title;
let clusterVersion = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(clusterName)?.kwirthData.version || "0.0.0";
let queryUrl = void 0;
if (entity.metadata.annotations["backstage.io/kubernetes-id"]) {
queryUrl = url + `/managecluster/find?label=backstage.io%2fkubernetes-id&entity=${entity.metadata.annotations["backstage.io/kubernetes-id"]}&type=pod&data=containers`;
} else if (entity.metadata.annotations["backstage.io/kubernetes-label-selector"]) {
if (kwirthCommon.versionGreaterThan(clusterVersion, "0.4.40")) {
let escapedLabelSelector = encodeURIComponent(entity.metadata.annotations["backstage.io/kubernetes-label-selector"]);
queryUrl = url + `/managecluster/find?labelselector=${escapedLabelSelector}&type=pod&data=containers`;
} else {
loggerSvc.error(`Version ${clusterVersion} from cluster ${clusterName} is not valid for using ${pluginKwirthCommon.ANNOTATION_BACKSTAGE_KUBERNETES_LABELSELECTOR}`);
clusterList.push({ name: clusterName, url, title, pods: [], accessKeys: /* @__PURE__ */ new Map() });
continue;
}
} else {
loggerSvc.error("Received request without labelid/labelselector");
clusterList.push({ name: clusterName, url, title, pods: [], accessKeys: /* @__PURE__ */ new Map() });
continue;
}
try {
let fetchResp = await fetch(queryUrl, { headers: { "Authorization": "Bearer " + apiKeyStr } });
if (fetchResp.status === 200) {
let jsonResp = await fetchResp.json();
if (jsonResp) {
let podData = {
name: clusterName,
url,
title,
pods: jsonResp,
accessKeys: /* @__PURE__ */ new Map()
};
clusterList.push(podData);
} else {
loggerSvc.warn(`Invalid data received from cluster ${clusterName}`);
clusterList.push({ name: clusterName, url, title, pods: [], accessKeys: /* @__PURE__ */ new Map() });
}
} else {
loggerSvc.warn(`Invalid response from cluster ${clusterName}: ${fetchResp.status}`);
let text = await fetchResp.text();
if (text) loggerSvc.warn(text);
clusterList.push({ name: clusterName, url, title, pods: [], accessKeys: /* @__PURE__ */ new Map() });
}
} catch (err) {
loggerSvc.warn(`Cannot access cluster ${clusterName} (URL: ${queryUrl}): ${err}`);
clusterList.push({ name: clusterName, url, title, pods: [], accessKeys: /* @__PURE__ */ new Map() });
}
}
return clusterList;
};
const createAccessKey = async (reqScope, cluster, reqPods, userName) => {
let resources = reqPods.map((podData) => `${reqScope}:${podData.namespace}::${podData.name}:`).join(";");
let kwirthHome = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(cluster.name)?.kwirthHome;
let kwirthApiKey = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(cluster.name)?.kwirthApiKey;
let payload = {
description: `Backstage API key for user ${userName}`,
expire: Date.now() + 60 * 60 * 1e3,
days: 1,
accessKey: {
id: "",
type: "bearer",
resources
}
};
let fetchResp = await fetch(kwirthHome + "/key", { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", Authorization: "Bearer " + kwirthApiKey } });
if (fetchResp.status === 200) {
let data = await fetchResp.json();
return data.accessKey;
} else {
loggerSvc.warn(`Invalid response asking for a key from cluster ${cluster.name}: ${fetchResp.status}`);
return void 0;
}
};
const addAccessKeys = async (channel, reqScope, foundClusters, entityName, userEntityRef, userGroups) => {
if (!reqScope) {
loggerSvc.info(`Invalid scope requested: ${reqScope}`);
return;
}
let principal = userEntityRef.split(":")[1];
let username = principal.split("/")[1];
for (let foundCluster of foundClusters) {
let podList = [];
if (!KwirthStaticData.KwirthStaticData.clusterKwirthData.get(foundCluster.name)?.kwirthData.channels.some((c) => c.id === channel)) {
loggerSvc.warn(`Cluster ${foundCluster.name} does not implement channel ${channel} (requested scope: ${reqScope})`);
continue;
}
for (let podData of foundCluster.pods) {
let allowedToNamespace = permissions.checkNamespaceAccess(channel, foundCluster, podData, userEntityRef, userGroups);
if (allowedToNamespace) {
let clusterDef = KwirthStaticData.KwirthStaticData.clusterKwirthData.get(foundCluster.name);
let podPermissionSet = permissions.getPodPermissionSet(channel, clusterDef);
if (!podPermissionSet) {
loggerSvc.warn(`Pod permission set not found: ${channel}`);
continue;
}
let namespaceRestricted = podPermissionSet.some((pp) => pp.namespace === podData.namespace);
if (!namespaceRestricted || permissions.checkPodAccess(podData, podPermissionSet, entityName, userEntityRef, userGroups)) {
podList.push(podData);
}
}
}
if (podList.length > 0) {
let accessKey = await createAccessKey(reqScope, foundCluster, podList, username);
if (accessKey) foundCluster.accessKeys.set(reqScope, accessKey);
} else {
loggerSvc.info(`No pods on podList for '${reqScope}' on channel '${channel}' in cluster '${foundCluster.name}' for searching for entity: '${entityName}'`);
}
}
};
const getUserGroups = async (userInfo) => {
const { token } = await authSvc.getPluginRequestToken({
onBehalfOf: await authSvc.getOwnServiceCredentials(),
targetPluginId: "catalog"
});
const catalogClient$1 = new catalogClient.CatalogClient({
discoveryApi: discoverySvc,
fetchApi: createAuthFetchApi(token)
});
const entity = await catalogClient$1.getEntityByRef(userInfo.userEntityRef);
let userGroupsRefs = [];
if (entity?.spec.memberOf) userGroupsRefs = entity?.spec.memberOf;
return userGroupsRefs;
};
const processVersion = async (_req, res) => {
res.status(200).send({ version: version.VERSION });
};
const processInfo = async (_req, res) => {
res.status(200).send(
KwirthStaticData.KwirthStaticData.latestVersions
);
};
const processAccess = async (req, res) => {
if (!req.query["scopes"] || !req.query["channel"]) {
res.status(400).send(`'scopes' and 'channel' are required`);
return;
}
let reqScopes = req.query["scopes"].toString().split(",");
let reqChannel = req.query["channel"]?.toString();
const credentials = await httpAuthSvc.credentials(req, { allow: ["user"] });
const userInfo = await userInfoSvc.getUserInfo(credentials);
let userGroupsRefs = await getUserGroups(userInfo);
loggerSvc.info(`Checking reqScopes '${req.query["scopes"]}' scopes for working with pod: '${req.body.metadata.namespace + "/" + req.body.metadata.name}' for user '${userInfo.userEntityRef}'`);
let foundClusters = await getValidClustersFromEntity(req.body);
for (let reqScopeStr of reqScopes) {
let reqScope = reqScopeStr;
await addAccessKeys(reqChannel, reqScope, foundClusters, req.body.metadata.name, userInfo.userEntityRef, userGroupsRefs);
if (reqScope === kwirthCommon.InstanceConfigScopeEnum.STREAM) {
for (let cluster of foundClusters) {
let accessKey = cluster.accessKeys.get(kwirthCommon.InstanceConfigScopeEnum.STREAM);
if (accessKey) {
let url = cluster.url + "/metrics";
let auth = "Bearer " + kwirthCommon.accessKeySerialize(accessKey);
let fetchResp = await fetch(url, { headers: { "Authorization": auth } });
try {
let data = await fetchResp.json();
cluster.metrics = data;
} catch (err) {
loggerSvc.error(`Cannot get metrics on cluster ${cluster.name}: ` + err);
}
}
}
}
}
for (let c of foundClusters) {
c.accessKeys = JSON.stringify(Array.from(c.accessKeys.entries()));
}
res.status(200).send(foundClusters);
};
router.post(["/access"], (req, res) => {
processAccess(req, res);
});
router.get(["/version"], (req, res) => {
processVersion(req, res);
});
router.get(["/info"], (req, res) => {
processInfo(req, res);
});
return router;
}
exports.createRouter = createRouter;
//# sourceMappingURL=router.cjs.js.map