@gorizond/catalog-backend-module-fleet
Version:
Backstage catalog backend module for Rancher Fleet GitOps entities
422 lines (421 loc) • 16.9 kB
JavaScript
"use strict";
/**
* Fleet Kubernetes Locator
*
* Dynamically discovers Rancher downstream clusters and exposes them as
* Backstage Kubernetes cluster definitions. Uses a single Rancher token that
* has access to all downstream clusters; does not mint per-cluster service
* accounts. Include `local` management cluster as well.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FleetK8sLocator = void 0;
const node_fetch_1 = __importDefault(require("node-fetch"));
const https_1 = __importDefault(require("https"));
/**
* Discover Rancher downstream clusters using a single Rancher token.
*/
class FleetK8sLocator {
constructor(opts) {
this.logger = opts.logger.child({ module: "fleet-k8s-locator" });
this.rancherUrl = opts.rancherUrl.replace(/\/$/, "");
this.rancherToken = opts.rancherToken;
this.skipTLSVerify = opts.skipTLSVerify;
this.includeLocal = opts.includeLocal;
this.fleetNamespaces = opts.fleetNamespaces;
}
static fromConfig({ logger, config, }) {
const enabled = config.getOptionalBoolean("catalog.providers.fleetK8sLocator.enabled");
if (enabled === false) {
logger.info("FleetK8sLocator disabled via config");
return undefined;
}
const rancherUrl = config.getOptionalString("catalog.providers.fleetK8sLocator.rancherUrl");
const rancherToken = config.getOptionalString("catalog.providers.fleetK8sLocator.rancherToken");
if (!rancherUrl || !rancherToken) {
logger.warn("FleetK8sLocator: missing rancherUrl or rancherToken; locator disabled");
return undefined;
}
const skipTLSVerify = config.getOptionalBoolean("catalog.providers.fleetK8sLocator.skipTLSVerify") ?? false;
const includeLocal = config.getOptionalBoolean("catalog.providers.fleetK8sLocator.includeLocal") ?? true;
const fleetNamespaces = config.getOptionalStringArray("catalog.providers.fleetK8sLocator.fleetNamespaces") ?? ["fleet-default", "fleet-local"];
return new FleetK8sLocator({
logger,
rancherUrl,
rancherToken,
skipTLSVerify,
includeLocal,
fleetNamespaces,
});
}
/**
* Returns cluster locator entries suitable for Backstage kubernetes plugin
* (type: config).
*/
async listClusters() {
const clusters = await this.fetchRancherClusters();
const bundleDeployments = await this.fetchBundleDeployments();
const customResourcesByCluster = this.buildCustomResourcesByCluster(bundleDeployments);
const entries = [];
for (const c of clusters) {
if (c.id === "local" && !this.includeLocal)
continue;
const apiUrl = `${this.rancherUrl}/k8s/clusters/${c.id}`;
const clusterName = c.name || c.id;
const cr = customResourcesByCluster.get(clusterName) ??
customResourcesByCluster.get(c.id) ??
[];
entries.push({
name: clusterName,
url: apiUrl,
authProvider: "serviceAccount",
serviceAccountToken: this.rancherToken,
caData: c.caCert,
skipTLSVerify: this.skipTLSVerify,
customResources: cr.length > 0 ? cr : undefined,
});
}
this.logger.debug(`FleetK8sLocator returning ${entries.length} clusters: ${entries
.map((c) => `${c.name} -> ${c.url}`)
.join(", ")}`);
return entries;
}
/**
* Returns lightweight cluster summaries (id + friendly name) without CRD scanning.
*/
async listClusterSummaries() {
const clusters = await this.fetchRancherClusters();
return clusters
.filter((c) => (this.includeLocal ? true : c.id !== "local"))
.map((c) => ({ id: c.id, name: c.name }));
}
async listRancherClusterDetails() {
const clusters = await this.fetchRancherClusters();
return clusters.filter((c) => this.includeLocal ? true : c.id !== "local");
}
/**
* Return Rancher nodes grouped by cluster for use in catalog sync.
*/
async listClusterNodes() {
const clusters = await this.fetchRancherClusters();
const agent = this.buildAgent();
const results = [];
for (const cluster of clusters) {
if (!cluster.id)
continue;
if (cluster.id === "local" && !this.includeLocal)
continue;
try {
const nodes = await this.fetchClusterNodes(cluster.id, agent);
results.push({
clusterId: cluster.id,
clusterName: cluster.name,
nodes,
});
}
catch (e) {
this.logger.debug(`FleetK8sLocator failed to fetch nodes for cluster ${cluster.id}: ${e}`);
}
}
return results;
}
/**
* Return detailed Kubernetes nodes grouped by cluster (full Node objects).
*/
async listClusterNodesDetailed() {
const clusters = await this.fetchRancherClusters();
const results = [];
for (const cluster of clusters) {
if (!cluster.id)
continue;
if (cluster.id === "local" && !this.includeLocal)
continue;
const agent = this.buildAgent(cluster.caCert);
const base = `${this.rancherUrl}/k8s/clusters/${cluster.id}`;
try {
const data = await this.fetchJson(`${base}/api/v1/nodes?limit=500`, agent);
results.push({
clusterId: cluster.id,
clusterName: cluster.name,
nodes: data?.items ?? [],
});
}
catch (e) {
this.logger.debug(`FleetK8sLocator failed to fetch detailed nodes for cluster ${cluster.id}: ${e}`);
}
}
return results;
}
/**
* Return MachineDeployments grouped by cluster (if Cluster API is installed).
*/
async listClusterMachineDeployments() {
const clusters = await this.fetchRancherClusters();
const results = [];
for (const cluster of clusters) {
if (!cluster.id)
continue;
if (cluster.id === "local" && !this.includeLocal)
continue;
const agent = this.buildAgent(cluster.caCert);
const base = `${this.rancherUrl}/k8s/clusters/${cluster.id}`;
try {
const data = await this.fetchJson(`${base}/apis/cluster.x-k8s.io/v1beta1/machinedeployments?limit=500`, agent);
if (data?.items?.length) {
results.push({
clusterId: cluster.id,
clusterName: cluster.name,
items: data.items,
});
}
}
catch (e) {
this.logger.debug(`FleetK8sLocator failed to fetch MachineDeployments for cluster ${cluster.id}: ${e}`);
}
}
return results;
}
async listClusterVersions() {
const clusters = await this.fetchRancherClusters();
const results = [];
for (const cluster of clusters) {
if (!cluster.id)
continue;
if (cluster.id === "local" && !this.includeLocal)
continue;
const agent = this.buildAgent(cluster.caCert);
const base = `${this.rancherUrl}/k8s/clusters/${cluster.id}`;
try {
const data = await this.fetchJson(`${base}/version`, agent);
results.push({
clusterId: cluster.id,
clusterName: cluster.name,
version: data?.gitVersion,
});
}
catch (e) {
this.logger.debug(`FleetK8sLocator failed to fetch version for cluster ${cluster.id}: ${e}`);
}
}
return results;
}
async listHarvesterVirtualMachines() {
const clusters = await this.fetchRancherClusters();
let harvesterClusters = clusters.filter((c) => c.labels?.["provider.cattle.io"] === "harvester" ||
c.provider === "harvester" ||
c.driver === "harvester");
// Fallback: some clusters may not expose provider labels; try name/id heuristic
if (harvesterClusters.length === 0) {
harvesterClusters = clusters.filter((c) => c.name?.toLowerCase().includes("harvester") ||
c.id.toLowerCase().includes("harvester"));
if (harvesterClusters.length === 0) {
this.logger.debug("FleetK8sLocator: no Harvester clusters detected (by provider/driver or name heuristic)");
}
}
const results = [];
for (const cluster of harvesterClusters) {
if (!cluster.id)
continue;
if (cluster.id === "local" && !this.includeLocal)
continue;
const agent = this.buildAgent(cluster.caCert);
const base = `${this.rancherUrl}/k8s/clusters/${cluster.id}`;
try {
const data = await this.fetchJson(`${base}/apis/kubevirt.io/v1/virtualmachines?limit=500`, agent);
if (data?.items?.length) {
results.push({
clusterId: cluster.id,
clusterName: cluster.name,
items: data.items,
});
}
}
catch (e) {
this.logger.debug(`FleetK8sLocator failed to fetch Harvester VMs for cluster ${cluster.id}: ${e}`);
}
}
return results;
}
/**
* Convert to Backstage kubernetes.clusterLocatorMethods (type: config).
*/
async asClusterLocatorMethods() {
const clusters = await this.listClusters();
return [
{
type: "config",
clusters,
},
];
}
async fetchRancherClusters() {
const url = `${this.rancherUrl}/v3/clusters`;
this.logger.debug(`FleetK8sLocator fetching clusters from ${url}`);
const res = await (0, node_fetch_1.default)(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.rancherToken}`,
Accept: "application/json",
},
agent: this.skipTLSVerify === true
? new https_1.default.Agent({ rejectUnauthorized: false })
: undefined,
// TLS verify controlled by global agent (set NODE_TLS_REJECT_UNAUTHORIZED if needed)
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to fetch Rancher clusters: ${res.status} ${res.statusText} ${text}`);
}
const data = (await res.json());
this.logger.debug(`FleetK8sLocator received ${data.data?.length ?? 0} clusters`);
return data.data ?? [];
}
async fetchClusterNodes(clusterId, agent) {
const url = `${this.rancherUrl}/v3/clusters/${clusterId}/nodes`;
const res = await (0, node_fetch_1.default)(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.rancherToken}`,
Accept: "application/json",
},
agent,
});
if (!res.ok) {
const text = await res.text();
this.logger.debug(`FleetK8sLocator failed to fetch nodes for ${clusterId}: ${res.status} ${res.statusText} ${text}`);
return [];
}
const data = (await res.json());
return data.data ?? [];
}
buildAgent(caData) {
const agentOptions = {
rejectUnauthorized: this.skipTLSVerify ? false : true,
};
if (caData) {
try {
agentOptions.ca = Buffer.from(caData, "base64");
}
catch {
agentOptions.ca = caData;
}
}
return new https_1.default.Agent(agentOptions);
}
async fetchJson(url, agent) {
const res = await (0, node_fetch_1.default)(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.rancherToken}`,
Accept: "application/json",
},
agent,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`${res.status} ${res.statusText} ${text}`);
}
return (await res.json());
}
async fetchBundleDeployments() {
const deployments = [];
for (const ns of this.fleetNamespaces) {
const url = `${this.rancherUrl}/k8s/clusters/local/apis/fleet.cattle.io/v1alpha1/namespaces/${ns}/bundledeployments?limit=500`;
this.logger.debug(`FleetK8sLocator fetching BundleDeployments from ${ns} (${url})`);
const res = await (0, node_fetch_1.default)(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.rancherToken}`,
Accept: "application/json",
},
});
if (!res.ok) {
const text = await res.text();
this.logger.warn(`FleetK8sLocator failed to fetch BundleDeployments from ${ns}: ${res.status} ${res.statusText} ${text}`);
continue;
}
const data = (await res.json());
deployments.push(...(data.items ?? []));
}
return deployments;
}
buildCustomResourcesByCluster(bundleDeployments) {
const map = new Map();
for (const bd of bundleDeployments) {
const bdNamespace = bd?.metadata?.namespace ?? "";
const clusterName = extractClusterNameFromBundleDeploymentNamespace(bdNamespace);
const clusterId = bd?.metadata?.labels?.["fleet.cattle.io/cluster-name"] ?? clusterName;
if (!clusterName && !clusterId)
continue;
const resources = bd?.status?.resources ??
[];
for (const r of resources) {
const apiVersion = r?.apiVersion;
const kind = r?.kind;
if (!apiVersion || !kind)
continue;
const [group, version] = apiVersion.includes("/")
? apiVersion.split("/")
: ["", apiVersion];
// Skip core/built-in groups to avoid noise
if (group === "" || BUILTIN_GROUPS.has(group))
continue;
const plural = derivePluralFromKind(kind);
const matcher = {
group,
apiVersion: version,
plural,
};
const listKey = clusterId ?? clusterName;
const list = listKey ? (map.get(listKey) ?? []) : [];
if (!list.find((cr) => isSameCustomResource(cr, matcher))) {
list.push(matcher);
if (listKey) {
map.set(listKey, list);
}
if (clusterName && clusterId && clusterId !== clusterName) {
// Keep both keys pointing to the same array to avoid duplication
map.set(clusterName, list);
}
}
}
}
return map;
}
}
exports.FleetK8sLocator = FleetK8sLocator;
const BUILTIN_GROUPS = new Set([
"apps",
"batch",
"extensions",
"networking.k8s.io",
"policy",
"rbac.authorization.k8s.io",
"autoscaling",
"coordination.k8s.io",
"discovery.k8s.io",
"apiextensions.k8s.io",
"flowcontrol.apiserver.k8s.io",
"certificates.k8s.io",
"authentication.k8s.io",
"authorization.k8s.io",
]);
function extractClusterNameFromBundleDeploymentNamespace(namespace) {
const match = namespace.match(/^cluster-fleet-(?:default|local)-(.+)$/);
return match?.[1];
}
function derivePluralFromKind(kind) {
const base = kind.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
if (base.endsWith("s"))
return base;
if (base.endsWith("y"))
return `${base.slice(0, -1)}ies`;
return `${base}s`;
}
function isSameCustomResource(a, b) {
return (a.group === b.group &&
a.apiVersion === b.apiVersion &&
a.plural === b.plural);
}