UNPKG

@gorizond/catalog-backend-module-fleet

Version:

Backstage catalog backend module for Rancher Fleet GitOps entities

844 lines (843 loc) 35.2 kB
"use strict"; /** * Entity Mapper * Converts Fleet Custom Resources to Backstage Catalog Entities * * Mapping: * - Fleet Rancher Cluster (config) → Domain * - GitRepo → System * - Bundle → Component (type: service) * - BundleDeployment → Resource (type: fleet-deployment) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ANNOTATION_TECHDOCS_ENTITY = exports.ANNOTATION_KUBERNETES_LABEL_SELECTOR = exports.ANNOTATION_KUBERNETES_NAMESPACE = exports.ANNOTATION_KUBERNETES_ID = exports.ANNOTATION_FLEET_SOURCE_BUNDLE = exports.ANNOTATION_FLEET_SOURCE_GITREPO = exports.ANNOTATION_FLEET_CLUSTER = exports.ANNOTATION_FLEET_READY_CLUSTERS = exports.ANNOTATION_FLEET_STATUS = exports.ANNOTATION_FLEET_BUNDLE_PATH = exports.ANNOTATION_FLEET_REPO_NAME = exports.ANNOTATION_FLEET_TARGETS = exports.ANNOTATION_FLEET_NAMESPACE = exports.ANNOTATION_FLEET_BRANCH = exports.ANNOTATION_FLEET_REPO = void 0; exports.toBackstageName = toBackstageName; exports.toStableBackstageName = toStableBackstageName; exports.toEntityNamespace = toEntityNamespace; exports.mapFleetClusterToDomain = mapFleetClusterToDomain; exports.mapClusterToResource = mapClusterToResource; exports.mapNodeToResource = mapNodeToResource; exports.mapMachineDeploymentToResource = mapMachineDeploymentToResource; exports.mapVirtualMachineToResource = mapVirtualMachineToResource; exports.mapGitRepoToSystem = mapGitRepoToSystem; exports.mapBundleToComponent = mapBundleToComponent; exports.mapBundleDeploymentToResource = mapBundleDeploymentToResource; exports.mapApiDefinitionToApi = mapApiDefinitionToApi; exports.extractWorkspaceNamespaceFromBundleDeploymentNamespace = extractWorkspaceNamespaceFromBundleDeploymentNamespace; exports.extractBundleMetadata = extractBundleMetadata; exports.createEmptyBatch = createEmptyBatch; exports.flattenBatch = flattenBatch; const catalog_model_1 = require("@backstage/catalog-model"); const types_1 = require("./types"); const url_1 = require("url"); // ============================================================================ // Constants // ============================================================================ const FLEET_ANNOTATION_PREFIX = "fleet.cattle.io"; exports.ANNOTATION_FLEET_REPO = `${FLEET_ANNOTATION_PREFIX}/repo`; exports.ANNOTATION_FLEET_BRANCH = `${FLEET_ANNOTATION_PREFIX}/branch`; exports.ANNOTATION_FLEET_NAMESPACE = `${FLEET_ANNOTATION_PREFIX}/namespace`; exports.ANNOTATION_FLEET_TARGETS = `${FLEET_ANNOTATION_PREFIX}/targets`; exports.ANNOTATION_FLEET_REPO_NAME = `${FLEET_ANNOTATION_PREFIX}/repo-name`; exports.ANNOTATION_FLEET_BUNDLE_PATH = `${FLEET_ANNOTATION_PREFIX}/bundle-path`; exports.ANNOTATION_FLEET_STATUS = `${FLEET_ANNOTATION_PREFIX}/status`; exports.ANNOTATION_FLEET_READY_CLUSTERS = `${FLEET_ANNOTATION_PREFIX}/ready-clusters`; exports.ANNOTATION_FLEET_CLUSTER = `${FLEET_ANNOTATION_PREFIX}/cluster`; exports.ANNOTATION_FLEET_SOURCE_GITREPO = `${FLEET_ANNOTATION_PREFIX}/source-gitrepo`; exports.ANNOTATION_FLEET_SOURCE_BUNDLE = `${FLEET_ANNOTATION_PREFIX}/source-bundle`; // Backstage Kubernetes integration annotations exports.ANNOTATION_KUBERNETES_ID = "backstage.io/kubernetes-id"; exports.ANNOTATION_KUBERNETES_NAMESPACE = "backstage.io/kubernetes-namespace"; exports.ANNOTATION_KUBERNETES_LABEL_SELECTOR = "backstage.io/kubernetes-label-selector"; exports.ANNOTATION_TECHDOCS_ENTITY = "backstage.io/techdocs-entity"; // ============================================================================ // Entity Naming // ============================================================================ const crypto_1 = require("crypto"); /** * Convert a name to Backstage-safe entity name * Must match: [a-z0-9]+(-[a-z0-9]+)* */ const MAX_ENTITY_NAME_LENGTH = 63; function sanitizeName(value) { return value .toLowerCase() .replace(/[^a-z0-9-]/g, "-") .replace(/--+/g, "-") .replace(/^-+/, "") .replace(/-+$/, ""); } function toBackstageName(value) { const clean = sanitizeName(value); const trimmed = clean.slice(0, MAX_ENTITY_NAME_LENGTH).replace(/-+$/, ""); return trimmed || "fleet-entity"; } /** * Convert a name to Backstage-safe entity name with truncation + hash * to keep it short while preserving uniqueness and ending rules. */ function toStableBackstageName(value, maxLength = MAX_ENTITY_NAME_LENGTH) { const clean = sanitizeName(value); const fallback = "fleet-entity"; if (!clean) return fallback; if (clean.length <= maxLength) return clean; const hash = (0, crypto_1.createHash)("sha1").update(clean).digest("hex").slice(0, 6); const base = clean .slice(0, Math.max(1, maxLength - hash.length - 1)) .replace(/-+$/, ""); const result = `${base}-${hash}`.replace(/-+$/, ""); return result || `${fallback}-${hash}`; } /** * Create entity namespace from Fleet namespace */ function toEntityNamespace(fleetNamespace) { return toBackstageName(fleetNamespace); } function deriveOwnerFromRepo(repo) { if (!repo) return undefined; try { const url = new url_1.URL(repo); const segments = url.pathname.split("/").filter(Boolean); const ownerSegment = segments[0]; if (!ownerSegment) return undefined; return `group:default/${toBackstageName(ownerSegment)}`; } catch { return undefined; } } function deriveKubernetesNamespace(fleetYaml, fallback) { return (fleetYaml?.defaultNamespace ?? fleetYaml?.namespace ?? fallback ?? "default"); } function deriveNamespaceFromStatus(gitRepo) { const resources = gitRepo.status?.resources ?? []; for (const r of resources) { if (r.namespace) return r.namespace; } return undefined; } // ============================================================================ // Fleet Cluster (config) → Domain // Represents the Rancher Fleet management cluster (e.g., rancher.example.com) // ============================================================================ function mapFleetClusterToDomain(context, entityNamespace = "default") { const cluster = context.cluster; const name = toBackstageName(cluster.name); // Extract hostname from URL for description let hostname = cluster.url; try { hostname = new url_1.URL(cluster.url).hostname; } catch { // Keep original URL if parsing fails } const description = `Fleet Rancher Cluster: ${hostname}`; const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_CLUSTER]: cluster.name, [`${FLEET_ANNOTATION_PREFIX}/url`]: cluster.url, }; // Add namespace list const namespaceNames = cluster.namespaces.map((ns) => ns.name); annotations[`${FLEET_ANNOTATION_PREFIX}/namespaces`] = namespaceNames.join(","); const tags = ["fleet", "rancher", "gitops"]; return { apiVersion: "backstage.io/v1alpha1", kind: "Domain", metadata: { name, namespace: entityNamespace, description, annotations, tags, links: [{ url: cluster.url, title: "Rancher Fleet" }], }, spec: { owner: "platform-team", }, }; } // ============================================================================ // Cluster (downstream) → Resource (type: kubernetes-cluster) // ============================================================================ function mapClusterToResource(clusterId, clusterName, namespace, context, details) { const safeName = toBackstageName(clusterName ?? clusterId); const entityNamespace = toEntityNamespace(namespace); const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_CLUSTER]: clusterId, [exports.ANNOTATION_KUBERNETES_ID]: clusterId, }; if (details?.version) { annotations[`${FLEET_ANNOTATION_PREFIX}/kubernetes-version`] = details.version; } if (details?.nodeCount !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-count`] = String(details.nodeCount); } if (details?.readyNodeCount !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-ready-count`] = String(details.readyNodeCount); } if (details?.machineDeploymentCount !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployments`] = String(details.machineDeploymentCount); } if (details?.vmCount !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/virtual-machines`] = String(details.vmCount); } if (details?.state) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-state`] = details.state; } if (details?.transitioning) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-transitioning`] = details.transitioning; } if (details?.transitioningMessage) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-transitioning-message`] = details.transitioningMessage; } if (details?.conditions) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-conditions`] = JSON.stringify(details.conditions); } if (details?.etcdBackupConfig) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-etcd-backup-config`] = JSON.stringify(details.etcdBackupConfig); } if (details?.driver) { annotations[`${FLEET_ANNOTATION_PREFIX}/cluster-driver`] = details.driver; } const versionSuffix = details?.version ? ` (v${details.version})` : ""; const description = `Downstream Kubernetes cluster: ${clusterName ?? clusterId}${versionSuffix}`; return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { name: safeName, namespace: entityNamespace, description, annotations, tags: ["fleet", "kubernetes-cluster"], }, spec: { type: "kubernetes-cluster", owner: "platform-team", }, }; } // ============================================================================ // Node (downstream) → Resource (type: kubernetes-node) // ============================================================================ function mapNodeToResource(params) { const { nodeId, nodeName, clusterId, clusterName, workspaceNamespace, context, details, } = params; const safeName = toStableBackstageName(nodeName ?? nodeId, 63); const entityNamespace = toEntityNamespace(workspaceNamespace); const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_CLUSTER]: clusterId, [`${FLEET_ANNOTATION_PREFIX}/node-id`]: nodeId, [exports.ANNOTATION_KUBERNETES_ID]: clusterId, }; const dependsOn = [ (0, catalog_model_1.stringifyEntityRef)({ kind: "Resource", namespace: entityNamespace, name: toBackstageName(clusterName ?? clusterId), }), ]; if (details?.harvesterVmRef) { annotations[`${FLEET_ANNOTATION_PREFIX}/harvester-vm-ref`] = details.harvesterVmRef; dependsOn.push(details.harvesterVmRef); } if (details?.labels) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-labels`] = JSON.stringify(details.labels); } if (details?.taints) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-taints`] = JSON.stringify(details.taints); } if (details?.capacity) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-capacity`] = JSON.stringify(details.capacity); } if (details?.allocatable) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-allocatable`] = JSON.stringify(details.allocatable); } if (details?.addresses) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-addresses`] = JSON.stringify(details.addresses); } if (details?.providerId) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-provider-id`] = details.providerId; } if (details?.kubeletVersion) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-kubelet-version`] = details.kubeletVersion; } if (details?.osImage) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-os-image`] = details.osImage; } if (details?.containerRuntime) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-cri`] = details.containerRuntime; } if (details?.architecture) { annotations[`${FLEET_ANNOTATION_PREFIX}/node-arch`] = details.architecture; } return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { name: safeName, namespace: entityNamespace, description: `Kubernetes node ${nodeName ?? nodeId} in cluster ${clusterName ?? clusterId}`, annotations, tags: [ "fleet", "kubernetes-node", `cluster-${toBackstageName(clusterName ?? clusterId)}`, ], }, spec: { type: "kubernetes-node", owner: "platform-team", dependsOn, }, }; } // ============================================================================ // MachineDeployment (Cluster API) → Resource (type: kubernetes-machinedeployment) // ============================================================================ function mapMachineDeploymentToResource(params) { const { mdName, clusterId, clusterName, workspaceNamespace, context, details, } = params; const safeName = toStableBackstageName(mdName, 63); const entityNamespace = toEntityNamespace(workspaceNamespace); const clusterRef = (0, catalog_model_1.stringifyEntityRef)({ kind: "Resource", namespace: entityNamespace, name: toBackstageName(clusterName ?? clusterId), }); const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_CLUSTER]: clusterId, [`${FLEET_ANNOTATION_PREFIX}/machine-deployment`]: mdName, [exports.ANNOTATION_KUBERNETES_ID]: clusterId, }; if (details?.namespace) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-namespace`] = details.namespace; } if (details?.labels) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-labels`] = JSON.stringify(details.labels); } if (details?.selector) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-selector`] = JSON.stringify(details.selector); } if (details?.replicas !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-replicas`] = String(details.replicas); } if (details?.availableReplicas !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-available`] = String(details.availableReplicas); } if (details?.readyReplicas !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-ready`] = String(details.readyReplicas); } if (details?.updatedReplicas !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/machine-deployment-updated`] = String(details.updatedReplicas); } return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { name: safeName, namespace: entityNamespace, description: `MachineDeployment ${mdName} in cluster ${clusterName ?? clusterId}`, annotations, tags: ["fleet", "cluster-api", "kubernetes-machinedeployment"], }, spec: { type: "kubernetes-machinedeployment", owner: "platform-team", dependsOn: [clusterRef], }, }; } // ============================================================================ // Harvester VirtualMachine (KubeVirt) → Resource (type: kubernetes-virtual-machine) // ============================================================================ function mapVirtualMachineToResource(params) { const { vmName, entityName, clusterId, clusterName, workspaceNamespace, context, details, } = params; const safeName = entityName ?? toStableBackstageName(vmName, 63); const entityNamespace = toEntityNamespace(workspaceNamespace); const clusterRef = (0, catalog_model_1.stringifyEntityRef)({ kind: "Resource", namespace: entityNamespace, name: toBackstageName(clusterName ?? clusterId), }); const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_CLUSTER]: clusterId, [`${FLEET_ANNOTATION_PREFIX}/virtual-machine`]: vmName, [exports.ANNOTATION_KUBERNETES_ID]: clusterId, }; if (details?.namespace) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-namespace`] = details.namespace; } if (details?.labels) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-labels`] = JSON.stringify(details.labels); } if (details?.requests) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-requests`] = JSON.stringify(details.requests); } if (details?.limits) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-limits`] = JSON.stringify(details.limits); } if (details?.runStrategy) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-run-strategy`] = details.runStrategy; } if (details?.printableStatus) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-status`] = details.printableStatus; } if (details?.ready !== undefined) { annotations[`${FLEET_ANNOTATION_PREFIX}/vm-ready`] = String(details.ready); } return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { name: safeName, namespace: entityNamespace, description: `Harvester VM ${vmName} in cluster ${clusterName ?? clusterId}`, annotations, tags: ["fleet", "harvester", "kubevirt-vm"], }, spec: { type: "kubernetes-virtual-machine", owner: "platform-team", dependsOn: [clusterRef], }, }; } // ============================================================================ // GitRepo → System // ============================================================================ function mapGitRepoToSystem(gitRepo, context) { const name = toBackstageName(gitRepo.metadata?.name ?? "fleet-gitrepo"); const namespace = toEntityNamespace(gitRepo.metadata?.namespace ?? "fleet-default"); const fleetYaml = context.fleetYaml; const targets = gitRepo.spec?.targets?.map((t) => t.name).filter(Boolean) ?? []; const status = gitRepo.status?.display?.state ?? "Unknown"; const descriptionFromRepo = gitRepo.metadata?.annotations?.["field.cattle.io/description"] ?? gitRepo.metadata?.annotations?.["description"] ?? `Fleet GitRepo: ${gitRepo.spec?.repo ?? "unknown"}`; const description = fleetYaml?.backstage?.description ?? descriptionFromRepo; const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_REPO]: gitRepo.spec?.repo ?? "", [exports.ANNOTATION_FLEET_BRANCH]: gitRepo.spec?.branch ?? "main", [exports.ANNOTATION_FLEET_NAMESPACE]: gitRepo.metadata?.namespace ?? "", [exports.ANNOTATION_FLEET_CLUSTER]: context.cluster.name, [exports.ANNOTATION_FLEET_STATUS]: status, }; if (targets.length > 0) { annotations[exports.ANNOTATION_FLEET_TARGETS] = JSON.stringify(targets); } if (gitRepo.spec?.repo) { annotations[catalog_model_1.ANNOTATION_SOURCE_LOCATION] = `url:${gitRepo.spec.repo}`; } if (gitRepo.status?.display?.readyClusters) { annotations[exports.ANNOTATION_FLEET_READY_CLUSTERS] = gitRepo.status.display.readyClusters; } const kubeNamespace = deriveNamespaceFromStatus(gitRepo) ?? deriveKubernetesNamespace(fleetYaml, gitRepo.metadata?.namespace); const gitRepoName = gitRepo.metadata?.name; if (kubeNamespace) { annotations[exports.ANNOTATION_KUBERNETES_NAMESPACE] = kubeNamespace; } if (gitRepoName) { annotations[exports.ANNOTATION_KUBERNETES_LABEL_SELECTOR] = `app.kubernetes.io/name=${gitRepoName}`; } // Merge custom annotations from fleet.yaml if (fleetYaml?.annotations) { Object.assign(annotations, fleetYaml.annotations); } if (fleetYaml?.backstage?.annotations) { Object.assign(annotations, fleetYaml.backstage.annotations); } if (context.autoTechdocsRef !== false && gitRepo.spec?.repo && !annotations["backstage.io/techdocs-ref"]) { const repo = gitRepo.spec.repo.replace(/\/$/, ""); const branch = gitRepo.spec.branch ?? "main"; annotations["backstage.io/techdocs-ref"] = `url:${repo}/-/tree/${branch}`; } const tags = ["fleet", "gitops", ...(fleetYaml?.backstage?.tags ?? [])]; const links = []; if (gitRepo.spec?.repo) { links.push({ url: gitRepo.spec.repo, title: "Git Repository" }); } // Build dependsOn relations from fleet.yaml const dependsOn = []; if (fleetYaml?.backstage?.dependsOn) { dependsOn.push(...fleetYaml.backstage.dependsOn); } if (fleetYaml?.dependsOn) { dependsOn.push(...mapFleetDependsOn(fleetYaml.dependsOn, namespace)); } // Build API relations const providesApis = []; const consumesApis = []; if (fleetYaml?.backstage?.providesApis) { for (const api of fleetYaml.backstage.providesApis) { const apiRef = (0, catalog_model_1.stringifyEntityRef)({ kind: "API", namespace, name: toBackstageName(api.name), }); providesApis.push(apiRef); } } if (fleetYaml?.backstage?.consumesApis) { consumesApis.push(...fleetYaml.backstage.consumesApis); } // Domain relation to parent Fleet Cluster const domain = (0, catalog_model_1.stringifyEntityRef)({ kind: "Domain", namespace: "default", name: toBackstageName(context.cluster.name), }); const derivedOwner = fleetYaml?.backstage?.owner ?? deriveOwnerFromRepo(gitRepo.spec?.repo) ?? "group:default/default"; const spec = { lifecycle: (0, types_1.statusToLifecycle)(status), owner: derivedOwner, domain, }; if (dependsOn.length > 0) { spec.dependsOn = [...new Set(dependsOn)]; } if (providesApis.length > 0) { spec.providesApis = providesApis; } if (consumesApis.length > 0) { spec.consumesApis = consumesApis; } return { apiVersion: "backstage.io/v1alpha1", kind: "System", metadata: { name, namespace, description, annotations, tags: [...new Set(tags)], links, }, spec, }; } // ============================================================================ // Bundle → Component (type: service) // ============================================================================ function mapBundleToComponent(bundle, context) { const name = toBackstageName(bundle.metadata?.name ?? "fleet-bundle"); const namespace = toEntityNamespace(bundle.metadata?.namespace ?? "fleet-default"); const fleetYaml = context.fleetYaml; const gitRepoName = bundle.metadata?.labels?.["fleet.cattle.io/repo-name"]; const bundlePath = bundle.metadata?.labels?.["fleet.cattle.io/bundle-path"]; const status = bundle.status?.display?.state ?? "Unknown"; const systemRef = gitRepoName ? (0, catalog_model_1.stringifyEntityRef)({ kind: "System", namespace, name: toBackstageName(gitRepoName), }) : undefined; const description = fleetYaml?.backstage?.description ?? `Fleet Bundle: ${bundle.metadata?.name ?? "unknown"}`; const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_STATUS]: status, [exports.ANNOTATION_FLEET_CLUSTER]: context.cluster.name, }; if (gitRepoName) { annotations[exports.ANNOTATION_FLEET_REPO_NAME] = gitRepoName; annotations[exports.ANNOTATION_FLEET_SOURCE_GITREPO] = gitRepoName; } if (bundlePath) { annotations[exports.ANNOTATION_FLEET_BUNDLE_PATH] = bundlePath; } if (bundle.status?.display?.readyClusters) { annotations[exports.ANNOTATION_FLEET_READY_CLUSTERS] = bundle.status.display.readyClusters; } // Kubernetes integration annotations for Backstage K8s plugin // Determine the target namespace where resources will be deployed const targetNamespace = bundle.spec?.targetNamespace ?? bundle.spec?.defaultNamespace ?? bundle.spec?.namespace ?? fleetYaml?.targetNamespace ?? fleetYaml?.defaultNamespace ?? fleetYaml?.namespace ?? "default"; const helmReleaseName = fleetYaml?.helm?.releaseName ?? bundle.metadata?.name; const objectsetHash = bundle.metadata?.labels?.["objectset.rio.cattle.io/hash"]; annotations[exports.ANNOTATION_KUBERNETES_NAMESPACE] = targetNamespace; // Prefer helm release name for Helm-based bundles (standard Helm label) if (helmReleaseName) { annotations[exports.ANNOTATION_KUBERNETES_LABEL_SELECTOR] = `app.kubernetes.io/instance=${helmReleaseName}`; } else if (objectsetHash) { // Fallback to objectset hash, but this may select too many resources annotations[exports.ANNOTATION_KUBERNETES_LABEL_SELECTOR] = `objectset.rio.cattle.io/hash=${objectsetHash}`; } // Merge custom annotations from fleet.yaml if (fleetYaml?.annotations) { Object.assign(annotations, fleetYaml.annotations); } if (fleetYaml?.backstage?.annotations) { Object.assign(annotations, fleetYaml.backstage.annotations); } const tags = ["fleet", "fleet-bundle", ...(fleetYaml?.backstage?.tags ?? [])]; // Build dependsOn relations - Bundle depends on its parent GitRepo (System) const dependsOn = []; // From Fleet bundle dependsOn if (bundle.spec?.dependsOn) { dependsOn.push(...mapFleetDependsOnToResource(bundle.spec.dependsOn, namespace)); } // From fleet.yaml dependsOn (Fleet native) if (fleetYaml?.dependsOn) { dependsOn.push(...mapFleetDependsOnToResource(fleetYaml.dependsOn, namespace)); } const spec = { type: fleetYaml?.backstage?.type ?? "service", lifecycle: fleetYaml?.backstage?.lifecycle ?? "production", owner: fleetYaml?.backstage?.owner ?? "unknown", system: systemRef, }; if (dependsOn.length > 0) { spec.dependsOn = [...new Set(dependsOn)]; } return { apiVersion: "backstage.io/v1alpha1", kind: "Component", metadata: { name, namespace, description, annotations: { ...annotations, ...(systemRef ? { [exports.ANNOTATION_TECHDOCS_ENTITY]: systemRef } : undefined), }, tags: [...new Set(tags)], }, spec, }; } // ============================================================================ // BundleDeployment → Resource (per-cluster deployment status) // ============================================================================ function mapBundleDeploymentToResource(bundleDeployment, clusterId, context, systemRef, clusterName) { const bdName = bundleDeployment.metadata?.name ?? "fleet-bundle-deployment"; const originalName = `${bdName}-${clusterId}`; const name = toStableBackstageName(originalName, 50); const namespace = toEntityNamespace(bundleDeployment.metadata?.namespace ?? "fleet-default"); const status = bundleDeployment.status?.display?.state ?? "Unknown"; const clusterDisplayName = clusterName ?? clusterId; const clusterResourceName = toBackstageName(clusterDisplayName); const description = `Fleet deployment: ${bdName} on cluster ${clusterDisplayName}`; const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_STATUS]: status, [exports.ANNOTATION_FLEET_CLUSTER]: clusterId, [`${FLEET_ANNOTATION_PREFIX}/bundle-deployment`]: bdName, [`${FLEET_ANNOTATION_PREFIX}/original-name`]: originalName, [`${FLEET_ANNOTATION_PREFIX}/target-cluster-id`]: clusterId, }; if (bundleDeployment.status?.display?.message) { annotations[`${FLEET_ANNOTATION_PREFIX}/message`] = bundleDeployment.status.display.message.slice(0, 500); } // Extract bundle name from labels const bundleName = bundleDeployment.metadata?.labels?.["fleet.cattle.io/bundle-name"]; if (bundleName) { annotations[exports.ANNOTATION_FLEET_SOURCE_BUNDLE] = bundleName; } const clusterWorkspaceNamespace = extractWorkspaceNamespaceFromBundleDeploymentNamespace(bundleDeployment.metadata?.namespace ?? "") ?? "default"; // BundleDeployment depends on the Bundle Component (logical workload) const dependsOn = []; if (bundleName) { const workspaceNamespace = extractWorkspaceNamespaceFromBundleDeploymentNamespace(bundleDeployment.metadata?.namespace ?? ""); dependsOn.push((0, catalog_model_1.stringifyEntityRef)({ kind: "Component", namespace: workspaceNamespace ? toEntityNamespace(workspaceNamespace) : toEntityNamespace(bundleDeployment.metadata?.namespace ?? ""), name: toBackstageName(bundleName), })); if (systemRef) { annotations[exports.ANNOTATION_TECHDOCS_ENTITY] = systemRef; } } // Also depend on Cluster entity dependsOn.push((0, catalog_model_1.stringifyEntityRef)({ kind: "Resource", namespace: clusterWorkspaceNamespace, name: clusterResourceName, })); return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { name, namespace, description, annotations, tags: ["fleet", "fleet-deployment", `cluster-${clusterResourceName}`], }, spec: { type: "fleet-deployment", owner: context.fleetYaml?.backstage?.owner ?? "unknown", dependsOn: dependsOn.length > 0 ? dependsOn : undefined, }, }; } // ============================================================================ // API Entity Generation (from fleet.yaml providesApis) // ============================================================================ function mapApiDefinitionToApi(apiDef, gitRepoName, context) { const name = toBackstageName(apiDef.name); const namespace = toEntityNamespace(context.fleetYaml?.defaultNamespace ?? "fleet-default"); const description = apiDef.description ?? `API ${apiDef.name} provided by ${gitRepoName}`; const annotations = { [catalog_model_1.ANNOTATION_LOCATION]: context.locationKey, [catalog_model_1.ANNOTATION_ORIGIN_LOCATION]: context.locationKey, [exports.ANNOTATION_FLEET_SOURCE_GITREPO]: gitRepoName, }; if (apiDef.definitionUrl) { annotations[`${FLEET_ANNOTATION_PREFIX}/definition-url`] = apiDef.definitionUrl; } // Determine API definition let definition = apiDef.definition ?? ""; if (!definition && apiDef.definitionUrl) { definition = `# API definition from: ${apiDef.definitionUrl}`; } if (!definition) { definition = `# No definition provided for ${apiDef.name}`; } return { apiVersion: "backstage.io/v1alpha1", kind: "API", metadata: { name, namespace, description, annotations, tags: ["fleet", "fleet-api"], }, spec: { type: apiDef.type ?? "openapi", lifecycle: (0, types_1.statusToLifecycle)(undefined), owner: context.fleetYaml?.backstage?.owner ?? "unknown", definition, }, }; } // ============================================================================ // Helper Functions // ============================================================================ function extractWorkspaceNamespaceFromBundleDeploymentNamespace(namespace) { const match = namespace.match(/^cluster-fleet-([^-]+)-.+$/); if (!match) return undefined; return `fleet-${match[1]}`; } /** * Map Fleet dependsOn to Backstage Component entity references */ function mapFleetDependsOn(fleetDependsOn, namespace) { const refs = []; for (const dep of fleetDependsOn) { if (dep.name) { refs.push((0, catalog_model_1.stringifyEntityRef)({ kind: "Component", namespace, name: toBackstageName(dep.name), })); } } return refs; } /** * Map Fleet dependsOn to Backstage Resource entity references (for Bundles) */ function mapFleetDependsOnToResource(fleetDependsOn, namespace) { const refs = []; for (const dep of fleetDependsOn) { if (dep.name) { refs.push((0, catalog_model_1.stringifyEntityRef)({ kind: "Resource", namespace, name: toBackstageName(dep.name), })); } } return refs; } /** * Extract entity metadata from bundle labels */ function extractBundleMetadata(bundle) { const labels = bundle.metadata?.labels ?? {}; return { gitRepoName: labels["fleet.cattle.io/repo-name"], bundlePath: labels["fleet.cattle.io/bundle-path"], commitId: labels["fleet.cattle.io/commit"], }; } function createEmptyBatch() { return { domains: [], systems: [], components: [], resources: [], apis: [], }; } function flattenBatch(batch) { return [ ...batch.domains, ...batch.systems, ...batch.components, ...batch.resources, ...batch.apis, ]; }