@gorizond/catalog-backend-module-fleet
Version:
Backstage catalog backend module for Rancher Fleet GitOps entities
844 lines (843 loc) • 35.2 kB
JavaScript
;
/**
* 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,
];
}