@gorizond/catalog-backend-module-fleet
Version:
Backstage catalog backend module for Rancher Fleet GitOps entities
787 lines (786 loc) • 36.3 kB
JavaScript
"use strict";
/**
* Fleet Entity Provider
* Provides Backstage Catalog entities from Rancher Fleet GitOps resources
*
* Entity Mapping:
* - Fleet Cluster (config) → Domain
* - GitRepo → System
* - Bundle → Component (type: service)
* - BundleDeployment → Resource (type: fleet-deployment)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FleetEntityProvider = void 0;
const luxon_1 = require("luxon");
const catalog_model_1 = require("@backstage/catalog-model");
const p_limit_1 = __importDefault(require("p-limit"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const https_1 = __importDefault(require("https"));
const FleetClient_1 = require("./FleetClient");
const entityMapper_1 = require("./entityMapper");
function deriveFriendlyClusterName(clusterId) {
// Fleet appends a random suffix to downstream cluster IDs (e.g., staging-000-edd3151847f4)
const match = clusterId.match(/^(.*?)-[a-f0-9]{12}$/);
return match ? match[1] : undefined;
}
// ============================================================================
// Fleet Entity Provider
// ============================================================================
class FleetEntityProvider {
addDiscoveredClustersToBatch(batch) {
if (!this.clusterNameMap || this.clusterNameMap.size === 0) {
return;
}
const cluster = this.clusters[0];
if (!cluster)
return;
const context = {
cluster,
locationKey: this.locationKey,
autoTechdocsRef: cluster.autoTechdocsRef,
};
for (const [clusterId, clusterName] of this.clusterNameMap.entries()) {
const stats = this.clusterStats?.get(clusterId);
const workspaceNamespace = this.getPrimaryWorkspace(clusterId);
const entity = (0, entityMapper_1.mapClusterToResource)(clusterId, clusterName, workspaceNamespace, context, stats);
batch.resources.push(entity);
}
}
async addNodesViaRancher(batch) {
if (!this.clusterNameMap || this.clusterNameMap.size === 0)
return;
const cfg = this.clusters[0];
if (!cfg?.url || !cfg?.token)
return;
const rancherBase = cfg.url.replace(/\/k8s\/clusters\/.+$/, "");
const agent = new https_1.default.Agent({
rejectUnauthorized: cfg.skipTLSVerify === true ? false : true,
});
for (const [clusterId, clusterName] of this.clusterNameMap.entries()) {
try {
const res = await (0, node_fetch_1.default)(`${rancherBase}/v3/clusters/${clusterId}/nodes`, {
method: "GET",
headers: {
Authorization: `Bearer ${cfg.token}`,
Accept: "application/json",
},
agent,
});
if (!res.ok) {
this.logger.debug(`Failed to fetch nodes for cluster ${clusterId}: ${res.status} ${res.statusText}`);
continue;
}
const data = (await res.json());
const nodes = data.data ?? [];
if (nodes.length === 0)
continue;
const cluster = this.clusters[0];
const context = {
cluster,
locationKey: this.locationKey,
autoTechdocsRef: cluster.autoTechdocsRef,
};
const workspaceNamespace = this.getPrimaryWorkspace(clusterId);
for (const node of nodes) {
const nodeId = node.id ?? node.nodeName ?? node.name;
if (!nodeId)
continue;
const nodeName = node.nodeName ?? node.hostname ?? node.name;
const entity = (0, entityMapper_1.mapNodeToResource)({
nodeId,
nodeName,
clusterId,
clusterName,
workspaceNamespace,
context,
});
batch.resources.push(entity);
}
const stats = this.clusterStats?.get(clusterId) ?? {};
stats.nodeCount = nodes.length;
this.clusterStats?.set(clusterId, stats);
}
catch (e) {
this.logger.debug(`Failed to load nodes for ${clusterId}: ${e}`);
}
}
}
/**
* Create FleetEntityProvider instances from configuration
*/
static fromConfig(config, options) {
const providersConfig = config.getOptionalConfig("catalog.providers.fleet");
if (!providersConfig) {
options.logger.info("No Fleet provider configuration found");
return [];
}
// Check if it's an array of providers or a single provider
const providerKeys = providersConfig.keys();
// If keys look like array indices or provider IDs
const isMultiProvider = providerKeys.some((key) => providersConfig.getOptionalConfig(key)?.has("clusters") ||
providersConfig.getOptionalConfig(key)?.has("namespaces"));
if (isMultiProvider) {
// Multiple named providers
return providerKeys.map((key) => {
const providerConfig = providersConfig.getConfig(key);
return FleetEntityProvider.createFromConfig(key, providerConfig, options);
});
}
// Single provider configuration
return [
FleetEntityProvider.createFromConfig("default", providersConfig, options),
];
}
static createFromConfig(id, config, options) {
const clusters = readClusters(config);
const schedule = readSchedule(config.getOptionalConfig("schedule"));
const concurrency = config.getOptionalNumber("concurrency") ?? 3;
options.logger.info(`Creating FleetEntityProvider[${id}] with ${clusters.length} cluster(s)`);
return new FleetEntityProvider({
id,
clusters,
schedule,
logger: options.logger,
concurrency,
k8sLocator: options.k8sLocator,
});
}
constructor(options) {
this.clusterWorkspaces = new Map();
this.clusterPrimaryWorkspace = new Map();
this.logger = options.logger.child({ plugin: "fleet-entity-provider" });
this.clusters = options.clusters;
this.schedule = options.schedule;
this.locationKey = `fleet:${options.id}`;
this.concurrency = options.concurrency ?? 3;
this.k8sLocator = options.k8sLocator;
}
getProviderName() {
return this.locationKey;
}
getSchedule() {
return this.schedule;
}
dedupeEntities(entities) {
const seen = new Set();
const result = [];
for (const entity of entities) {
const ns = entity.metadata?.namespace ?? "default";
const key = `${entity.kind}:${ns}:${entity.metadata?.name}`;
if (seen.has(key))
continue;
seen.add(key);
result.push(entity);
}
return result;
}
recordWorkspaceNamespace(clusterId, workspace) {
const trimmed = workspace || "default";
const set = this.clusterWorkspaces.get(clusterId) ?? new Set();
set.add(trimmed);
this.clusterWorkspaces.set(clusterId, set);
if (!this.clusterPrimaryWorkspace.has(clusterId)) {
this.clusterPrimaryWorkspace.set(clusterId, trimmed);
}
}
getPrimaryWorkspace(clusterId) {
const fromCluster = this.clusterPrimaryWorkspace.get(clusterId);
if (fromCluster)
return fromCluster;
const set = this.clusterWorkspaces.get(clusterId);
if (set && set.size > 0) {
if (set.has("fleet-default"))
return "fleet-default";
return Array.from(set.values())[0];
}
const fallback = clusterId === "local" ? "fleet-local" : "fleet-default";
this.clusterWorkspaces.set(clusterId, new Set([fallback]));
this.clusterPrimaryWorkspace.set(clusterId, fallback);
return fallback;
}
async populateClusterNameMap() {
this.clusterStats = new Map();
this.clusterWorkspaces.clear();
if (this.k8sLocator) {
try {
const clusters = await this.k8sLocator.listRancherClusterDetails();
if (clusters?.length) {
this.clusterNameMap = new Map(clusters.filter((c) => c.id).map((c) => [c.id, c.name ?? c.id]));
for (const c of clusters) {
if (!c.id)
continue;
const stats = this.clusterStats?.get(c.id) ?? {};
stats.state = c.state;
stats.transitioning = c.transitioning;
stats.transitioningMessage = c.transitioningMessage;
stats.conditions = c.conditions;
stats.etcdBackupConfig =
c.rancherKubernetesEngineConfig?.services?.etcd?.backupConfig;
stats.version =
stats.version ??
c.rancherKubernetesEngineConfig?.kubernetesVersion;
stats.driver = c.driver ?? c.labels?.["provider.cattle.io"];
this.clusterStats?.set(c.id, stats);
if (c.namespace) {
this.clusterPrimaryWorkspace.set(c.id, c.namespace);
}
}
this.logger.debug(`Loaded ${this.clusterNameMap.size} cluster names from FleetK8sLocator`);
return;
}
}
catch (e) {
this.logger.warn(`Failed to load cluster names via FleetK8sLocator: ${e}`);
}
}
try {
const cfg = this.clusters[0];
if (!cfg?.url || !cfg?.token)
return;
const rancherBase = cfg.url.replace(/\/k8s\/clusters\/.+$/, "");
const agent = new https_1.default.Agent({
rejectUnauthorized: cfg.skipTLSVerify === true ? false : true,
});
const res = await (0, node_fetch_1.default)(`${rancherBase}/v3/clusters`, {
method: "GET",
headers: {
Authorization: `Bearer ${cfg.token}`,
Accept: "application/json",
},
agent,
});
if (!res.ok) {
this.logger.warn(`Failed to fetch Rancher clusters for names: ${res.status} ${res.statusText}`);
return;
}
const data = (await res.json());
const entries = data.data ?? [];
this.clusterNameMap = new Map(entries
.filter((c) => c.id)
.map((c) => {
const isHarvester = c.labels?.["provider.cattle.io"] === "harvester";
const harvesterDisplay = c.annotations?.["provisioning.cattle.io/management-cluster-display-name"];
const displayName = c.displayName ??
c.annotations?.["field.cattle.io/displayName"] ??
c.name;
const friendly = isHarvester
? (harvesterDisplay ?? displayName ?? c.id)
: (displayName ?? c.id);
return [c.id, friendly];
}));
for (const c of entries) {
if (!c.id)
continue;
const stats = this.clusterStats?.get(c.id) ?? {};
stats.driver = c.driver ?? c.labels?.["provider.cattle.io"];
this.clusterStats?.set(c.id, stats);
if (c.namespace) {
this.clusterPrimaryWorkspace.set(c.id, c.namespace);
}
}
this.logger.debug(`Loaded ${this.clusterNameMap.size} cluster names from Rancher`);
}
catch (e) {
this.logger.warn(`Failed to load Rancher cluster names: ${e}`);
}
}
async collectClusterTopology(batch) {
if (!this.k8sLocator) {
await this.addNodesViaRancher(batch);
return;
}
const cluster = this.clusters[0];
if (!cluster)
return;
const context = {
cluster,
locationKey: this.locationKey,
autoTechdocsRef: cluster.autoTechdocsRef,
};
try {
const [nodeGroups, mdGroups, versions, vmGroups] = await Promise.all([
this.k8sLocator.listClusterNodesDetailed(),
this.k8sLocator.listClusterMachineDeployments(),
this.k8sLocator.listClusterVersions(),
this.k8sLocator.listHarvesterVirtualMachines(),
]);
const versionMap = new Map(versions.map((v) => [v.clusterId, v.version]));
// Index VMs by UID and name across all Harvester clusters to link nodes via providerID
const vmRefIndex = new Map();
for (const group of vmGroups) {
const vmWorkspace = this.getPrimaryWorkspace(group.clusterId);
const entityNamespace = (0, entityMapper_1.toEntityNamespace)(vmWorkspace);
const vmClusterName = this.clusterNameMap?.get(group.clusterId) ??
group.clusterName ??
group.clusterId;
for (const vm of group.items ?? []) {
const vmName = vm.metadata?.name;
const vmUid = vm.metadata?.uid;
const vmNamespace = vm.metadata?.namespace ?? "default";
if (!vmName)
continue;
const entityName = (0, entityMapper_1.toStableBackstageName)(`${vmName}-vm`, 63);
const key = `name:${vmNamespace}/${vmName}`;
const vmEntityRef = (0, catalog_model_1.stringifyEntityRef)({
kind: "Resource",
namespace: entityNamespace,
name: entityName,
});
vmRefIndex.set(key, vmEntityRef);
if (vmUid) {
vmRefIndex.set(`uid:${vmUid}`, vmEntityRef);
}
const requests = vm.spec?.template?.spec?.domain?.resources?.requests;
const limits = vm.spec?.template?.spec?.domain?.resources?.limits;
const entity = (0, entityMapper_1.mapVirtualMachineToResource)({
vmName,
entityName,
clusterId: group.clusterId,
clusterName: vmClusterName,
workspaceNamespace: vmWorkspace,
context,
details: {
namespace: vm.metadata?.namespace,
labels: vm.metadata?.labels,
requests: requests && Object.keys(requests).length ? requests : undefined,
limits: limits && Object.keys(limits).length ? limits : undefined,
runStrategy: vm.spec?.runStrategy,
printableStatus: vm.status?.printableStatus,
ready: vm.status?.ready,
},
});
batch.resources.push(entity);
}
}
for (const group of nodeGroups) {
const clusterId = group.clusterId;
const clusterName = this.clusterNameMap?.get(clusterId) ?? group.clusterName ?? clusterId;
const workspaceNamespace = this.getPrimaryWorkspace(clusterId);
const nodes = group.nodes ?? [];
let readyCount = 0;
for (const node of nodes) {
const nodeId = node.metadata?.uid ?? node.metadata?.name ?? node.spec?.providerID;
const nodeName = node.metadata?.name ?? nodeId;
if (!nodeId || !nodeName)
continue;
const addresses = node.status?.addresses;
const taints = node.spec?.taints;
const isReady = (node.status?.conditions ?? []).some((c) => c?.type === "Ready" && c?.status === "True");
if (isReady)
readyCount += 1;
let harvesterVmRef;
const providerId = node.spec?.providerID;
const match = providerId?.match(/^harvester:\/\/(.+)$/);
if (match) {
const suffix = match[1];
if (suffix.includes("/")) {
harvesterVmRef = vmRefIndex.get(`name:${suffix}`);
}
else {
harvesterVmRef =
vmRefIndex.get(`uid:${suffix}`) ??
vmRefIndex.get(`name:${suffix}`);
}
}
const entity = (0, entityMapper_1.mapNodeToResource)({
nodeId,
nodeName,
clusterId,
clusterName,
workspaceNamespace,
context,
details: {
labels: node.metadata?.labels ?? undefined,
capacity: node.status?.capacity ?? undefined,
allocatable: node.status?.allocatable ?? undefined,
taints,
addresses,
providerId: node.spec?.providerID,
kubeletVersion: node.status?.nodeInfo?.kubeletVersion,
osImage: node.status?.nodeInfo?.osImage,
containerRuntime: node.status?.nodeInfo?.containerRuntimeVersion,
architecture: node.status?.nodeInfo?.architecture,
harvesterVmRef,
},
});
batch.resources.push(entity);
}
const stats = this.clusterStats?.get(clusterId) ?? {};
stats.nodeCount = nodes.length;
stats.readyNodeCount = readyCount;
stats.version = versionMap.get(clusterId) ?? stats.version;
this.clusterStats?.set(clusterId, stats);
}
for (const group of mdGroups) {
const clusterId = group.clusterId;
const clusterName = this.clusterNameMap?.get(clusterId) ?? group.clusterName ?? clusterId;
const workspaceNamespace = this.getPrimaryWorkspace(clusterId);
const items = group.items ?? [];
for (const md of items) {
const mdName = md.metadata?.name;
if (!mdName)
continue;
const selector = md.spec?.selector?.matchLabels ?? {};
const labels = md.metadata?.labels ?? md.spec?.template?.metadata?.labels;
const entity = (0, entityMapper_1.mapMachineDeploymentToResource)({
mdName,
clusterId,
clusterName,
workspaceNamespace,
context,
details: {
namespace: md.metadata?.namespace,
labels: labels && Object.keys(labels).length ? labels : undefined,
selector: Object.keys(selector).length ? selector : undefined,
replicas: md.spec?.replicas,
availableReplicas: md.status?.availableReplicas,
readyReplicas: md.status?.readyReplicas,
updatedReplicas: md.status?.updatedReplicas,
},
});
batch.resources.push(entity);
}
const stats = this.clusterStats?.get(clusterId) ?? {};
stats.machineDeploymentCount = items.length;
stats.version = stats.version ?? versionMap.get(clusterId);
this.clusterStats?.set(clusterId, stats);
}
for (const group of vmGroups) {
const stats = this.clusterStats?.get(group.clusterId) ?? {};
const items = group.items ?? [];
stats.vmCount = items.length;
this.clusterStats?.set(group.clusterId, stats);
}
for (const v of versions) {
const stats = this.clusterStats?.get(v.clusterId) ?? {};
stats.version = v.version ?? stats.version;
this.clusterStats?.set(v.clusterId, stats);
}
}
catch (e) {
this.logger.debug(`Failed to collect cluster topology via k8s locator: ${e}`);
await this.addNodesViaRancher(batch);
}
}
async connect(connection) {
this.connection = connection;
this.logger.info(`Connected FleetEntityProvider[${this.locationKey}]`);
}
/**
* Main run method - fetches all Fleet resources and emits entities
*/
async run() {
if (!this.connection) {
throw new Error("FleetEntityProvider is not connected");
}
const startTime = Date.now();
this.logger.info(`FleetEntityProvider[${this.locationKey}] starting sync`);
const limit = (0, p_limit_1.default)(this.concurrency);
const batch = (0, entityMapper_1.createEmptyBatch)();
try {
await this.populateClusterNameMap();
await this.collectClusterTopology(batch);
this.addDiscoveredClustersToBatch(batch);
await Promise.all(this.clusters.map((cluster) => limit(async () => {
const clusterBatch = await this.fetchCluster(cluster);
batch.domains.push(...clusterBatch.domains);
batch.systems.push(...clusterBatch.systems);
batch.components.push(...clusterBatch.components);
batch.resources.push(...clusterBatch.resources);
batch.apis.push(...clusterBatch.apis);
})));
const entities = this.dedupeEntities((0, entityMapper_1.flattenBatch)(batch));
await this.connection.applyMutation({
type: "full",
entities: entities.map((entity) => ({
entity,
locationKey: this.locationKey,
})),
});
const duration = Date.now() - startTime;
this.logger.info(`FleetEntityProvider[${this.locationKey}] sync completed in ${duration}ms: ` +
`${batch.domains.length} domains, ${batch.systems.length} systems, ` +
`${batch.components.length} components, ${batch.resources.length} resources, ` +
`${batch.apis.length} APIs`);
}
catch (error) {
this.logger.error(`FleetEntityProvider[${this.locationKey}] sync failed: ${error}`);
throw error;
}
}
/**
* Fetch all Fleet resources from a single cluster
*/
async fetchCluster(cluster) {
const client = (0, FleetClient_1.createFleetClient)(cluster, this.logger);
const batch = (0, entityMapper_1.createEmptyBatch)();
this.logger.debug(`Fetching Fleet resources from cluster ${cluster.name}`);
const context = {
cluster,
locationKey: this.locationKey,
autoTechdocsRef: cluster.autoTechdocsRef,
};
// Create Domain entity for the Fleet Rancher Cluster itself
const domainEntity = (0, entityMapper_1.mapFleetClusterToDomain)(context);
batch.domains.push(domainEntity);
// Fetch GitRepos and Bundles from each namespace
for (const nsConfig of cluster.namespaces) {
const nsBatch = await this.fetchNamespace(client, cluster, nsConfig);
batch.systems.push(...nsBatch.systems);
batch.components.push(...nsBatch.components);
batch.resources.push(...nsBatch.resources);
batch.apis.push(...nsBatch.apis);
}
return batch;
}
/**
* Fetch all Fleet resources from a single namespace
*/
async fetchNamespace(client, cluster, nsConfig) {
const batch = (0, entityMapper_1.createEmptyBatch)();
const namespace = nsConfig.name;
// Build label selector
const labelSelector = (0, FleetClient_1.selectorToString)(cluster.gitRepoSelector ?? nsConfig.labelSelector);
this.logger.debug(`Fetching GitRepos from ${cluster.name}/${namespace} ` +
`(selector: ${labelSelector ?? "none"})`);
// Fetch GitRepos
const gitRepos = await client.listGitRepos({ namespace, labelSelector });
this.logger.debug(`Found ${gitRepos.length} GitRepos in ${namespace}`);
// Process each GitRepo
for (const gitRepo of gitRepos) {
const gitRepoBatch = await this.processGitRepo(client, cluster, gitRepo);
batch.systems.push(...gitRepoBatch.systems);
batch.components.push(...gitRepoBatch.components);
batch.resources.push(...gitRepoBatch.resources);
batch.apis.push(...gitRepoBatch.apis);
}
return batch;
}
/**
* Process a single GitRepo and its related resources
*/
async processGitRepo(client, cluster, gitRepo) {
const batch = (0, entityMapper_1.createEmptyBatch)();
const gitRepoName = gitRepo.metadata?.name ?? "unknown";
const namespace = gitRepo.metadata?.namespace ?? "fleet-default";
// Fetch fleet.yaml if configured
let fleetYaml;
if (cluster.fetchFleetYaml) {
fleetYaml = await this.fetchFleetYaml(gitRepo);
}
const context = {
cluster,
locationKey: this.locationKey,
fleetYaml,
autoTechdocsRef: cluster.autoTechdocsRef,
};
// Create System entity for GitRepo
const systemEntity = (0, entityMapper_1.mapGitRepoToSystem)(gitRepo, context);
batch.systems.push(systemEntity);
// Create API entities from fleet.yaml providesApis
if (cluster.generateApis && fleetYaml?.backstage?.providesApis) {
for (const apiDef of fleetYaml.backstage.providesApis) {
const apiEntity = (0, entityMapper_1.mapApiDefinitionToApi)(apiDef, gitRepoName, context);
batch.apis.push(apiEntity);
}
}
// Fetch and process Bundles
if (cluster.includeBundles !== false) {
const bundles = await client.listBundlesForGitRepo(namespace, gitRepoName);
this.logger.debug(`Found ${bundles.length} Bundles for GitRepo ${gitRepoName}`);
for (const bundle of bundles) {
const bundleBatch = await this.processBundle(client, cluster, bundle, fleetYaml);
batch.components.push(...bundleBatch.components);
batch.resources.push(...bundleBatch.resources);
}
}
return batch;
}
/**
* Process a single Bundle and its related resources
*/
async processBundle(client, cluster, bundle, fleetYaml) {
const batch = {
domains: [],
systems: [],
components: [],
resources: [],
apis: [],
};
const bundleName = bundle.metadata?.name ?? "unknown";
const context = {
cluster,
locationKey: this.locationKey,
fleetYaml,
autoTechdocsRef: cluster.autoTechdocsRef,
};
// Create Component entity for Bundle
const componentEntity = (0, entityMapper_1.mapBundleToComponent)(bundle, context);
batch.components.push(componentEntity);
const resourceRefs = [];
const parentSystemRef = typeof componentEntity.spec === "object"
? componentEntity.spec.system
: undefined;
// Fetch and process BundleDeployments (per-cluster status)
if (cluster.includeBundleDeployments) {
const deployments = await client.listBundleDeploymentsForBundle(bundleName);
this.logger.debug(`Found ${deployments.length} BundleDeployments for Bundle ${bundleName}`);
for (const bd of deployments) {
const clusterId = FleetClient_1.FleetClient.extractClusterIdFromNamespace(bd.metadata?.namespace ?? "");
if (clusterId) {
const fromBd = (0, entityMapper_1.extractWorkspaceNamespaceFromBundleDeploymentNamespace)(bd.metadata?.namespace ?? "");
if (fromBd) {
this.recordWorkspaceNamespace(clusterId, fromBd);
}
const workspaceNamespace = this.getPrimaryWorkspace(clusterId);
// Cluster entity
// Try to find friendly name from Rancher clusters if available
const derivedClusterId = deriveFriendlyClusterName(clusterId);
const clusterFriendlyName = this.clusterNameMap?.get(clusterId) ??
(derivedClusterId
? this.clusterNameMap?.get(derivedClusterId)
: undefined) ??
derivedClusterId ??
clusterId;
const clusterDetails = this.clusterStats?.get(clusterId);
const clusterResource = (0, entityMapper_1.mapClusterToResource)(clusterId, clusterFriendlyName, workspaceNamespace, context, clusterDetails);
batch.resources.push(clusterResource);
const bdResourceEntity = (0, entityMapper_1.mapBundleDeploymentToResource)(bd, clusterId, context, parentSystemRef, clusterFriendlyName);
batch.resources.push(bdResourceEntity);
resourceRefs.push((0, catalog_model_1.stringifyEntityRef)({
kind: "Resource",
namespace: bdResourceEntity.metadata.namespace ?? "default",
name: bdResourceEntity.metadata.name,
}));
}
}
}
if (resourceRefs.length > 0) {
const spec = (componentEntity.spec ?? {});
const existingDependsOn = Array.isArray(spec.dependsOn)
? spec.dependsOn
: [];
spec.dependsOn = [...new Set([...existingDependsOn, ...resourceRefs])];
componentEntity.spec = spec;
}
return batch;
}
/**
* Fetch fleet.yaml from Git repository
* Note: This is a placeholder - actual implementation would need Git access
*/
async fetchFleetYaml(gitRepo) {
// TODO: Implement actual fleet.yaml fetching from Git
// This would require:
// 1. Git credentials from Fleet secrets or separate config
// 2. Clone or fetch raw file from repo
// 3. Parse YAML
// For now, try to extract from annotations if available
const annotations = gitRepo.metadata?.annotations ?? {};
const fleetYamlRaw = annotations["fleet.cattle.io/fleet-yaml"];
if (fleetYamlRaw) {
try {
return JSON.parse(fleetYamlRaw);
}
catch {
this.logger.warn(`Failed to parse fleet.yaml annotation for ${gitRepo.metadata?.name}`);
}
}
return undefined;
}
}
exports.FleetEntityProvider = FleetEntityProvider;
// ============================================================================
// Configuration Readers
// ============================================================================
function readClusters(config) {
// Check for explicit clusters array
if (config.has("clusters")) {
const clustersConfig = config.getConfigArray("clusters");
return clustersConfig.map((clusterConfig) => buildClusterConfig(clusterConfig));
}
// Legacy/simple form: single cluster with properties at root level
return [buildClusterConfig(config)];
}
function buildClusterConfig(config) {
const name = config.getOptionalString("name") ??
config.getOptionalString("clusterName") ??
"local";
const url = config.getOptionalString("url") ??
config.getOptionalString("apiServer") ??
config.getOptionalString("clusterUrl") ??
"https://kubernetes.default.svc";
const namespaces = readNamespaces(config);
return {
name,
url,
token: config.getOptionalString("token"),
caData: config.getOptionalString("caData"),
skipTLSVerify: config.getOptionalBoolean("skipTLSVerify") ?? false,
namespaces,
includeBundles: config.getOptionalBoolean("includeBundles") ?? true,
includeBundleDeployments: config.getOptionalBoolean("includeBundleDeployments") ?? false,
generateApis: config.getOptionalBoolean("generateApis") ?? false,
fetchFleetYaml: config.getOptionalBoolean("fetchFleetYaml") ?? false,
gitRepoSelector: readSelector(config.getOptionalConfig("gitRepoSelector")),
autoTechdocsRef: config.getOptionalBoolean("autoTechdocsRef") ?? true,
};
}
function readNamespaces(config) {
// Simple string array
const asStrings = config.getOptionalStringArray("namespaces");
if (asStrings) {
return asStrings.map((ns) => ({ name: ns }));
}
// Config array with optional selectors
const asConfigs = config.getOptionalConfigArray("namespaces");
if (asConfigs) {
return asConfigs.map((nsConfig) => ({
name: nsConfig.getString("name"),
labelSelector: readSelector(nsConfig.getOptionalConfig("selector")),
}));
}
// Default namespace
return [{ name: "fleet-default" }];
}
function readSelector(config) {
if (!config) {
return undefined;
}
const matchLabelsConfig = config.getOptionalConfig("matchLabels");
const matchExpressions = config.getOptionalConfigArray("matchExpressions");
if (!matchLabelsConfig && !matchExpressions) {
return undefined;
}
const selector = {};
if (matchLabelsConfig) {
const matchLabels = {};
for (const key of matchLabelsConfig.keys()) {
matchLabels[key] = matchLabelsConfig.getString(key);
}
if (Object.keys(matchLabels).length > 0) {
selector.matchLabels = matchLabels;
}
}
if (matchExpressions) {
selector.matchExpressions = matchExpressions.map((expr) => ({
key: expr.getString("key"),
operator: expr.getString("operator"),
values: expr.getOptionalStringArray("values"),
}));
}
return Object.keys(selector).length > 0 ? selector : undefined;
}
function readSchedule(config) {
const frequencyMinutes = config?.getOptionalNumber("frequency.minutes") ?? 10;
const timeoutMinutes = config?.getOptionalNumber("timeout.minutes") ?? 5;
const initialDelaySeconds = config?.getOptionalNumber("initialDelay.seconds") ?? 15;
return {
frequency: luxon_1.Duration.fromObject({ minutes: frequencyMinutes }),
timeout: luxon_1.Duration.fromObject({ minutes: timeoutMinutes }),
initialDelay: luxon_1.Duration.fromObject({ seconds: initialDelaySeconds }),
};
}