UNPKG

@gorizond/catalog-backend-module-fleet

Version:

Backstage catalog backend module for Rancher Fleet GitOps entities

422 lines (421 loc) 16.9 kB
"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); }