UNPKG

@fireflyai/backstage-backend-plugin-firefly

Version:

Firefly backend plugin for Backstage

318 lines (312 loc) 11.8 kB
'use strict'; var crypto = require('crypto'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto); class FireflyEntityProvider { logger; fireflyClient; catalogService; config; auth; connection; filters; intervalMs; importSystems; importResources; tagKeysIdentifiers; correlateByComponentName; /** * Creates a new instance of the FireflyEntityProvider * @param options - Configuration options for the provider */ constructor(options) { this.logger = options.logger; this.fireflyClient = options.fireflyClient; this.catalogService = options.catalogService; this.auth = options.auth; this.config = options.config; const periodicCheckConfig = this.config.getOptionalConfig("firefly.periodicCheck"); this.filters = periodicCheckConfig?.getOptional("filters") || {}; this.intervalMs = (periodicCheckConfig?.getOptionalNumber("interval") || 3600) * 1e3; this.importSystems = periodicCheckConfig?.getOptionalBoolean("importSystems") || false; this.importResources = periodicCheckConfig?.getOptionalBoolean("importResources") || false; this.tagKeysIdentifiers = periodicCheckConfig?.getOptionalStringArray("tagKeysIdentifiers") || []; this.correlateByComponentName = periodicCheckConfig?.getOptionalBoolean("correlateByComponentName") || false; } /** * Returns the name of this provider * @inheritdoc */ getProviderName() { return "firefly"; } /** * Establishes a connection with the entity provider and starts the refresh cycle * @param connection - The connection to the entity catalog * @inheritdoc */ async connect(connection) { this.connection = connection; this.refresh(); if (this.intervalMs > 0) { setInterval(() => this.refresh(), this.intervalMs); } } /** * Reads all assets from Firefly and converts them into catalog entities * This method is called periodically to keep the catalog in sync with Firefly */ async refresh() { if (!this.connection) { throw new Error("Firefly entity provider is not initialized"); } try { this.logger.info("Refreshing Firefly assets"); const assets = await this.fireflyClient.getAllAssets(this.filters); this.logger.info(`Found ${assets.length} assets`); const components = await this.getAllComponents(); const resources = this.importResources ? await Promise.all(assets.map((asset) => this.assetToEntity(asset, components))) : []; const systems = this.importSystems ? this.getSystems(assets) : []; if (this.importResources) { this.logger.info(`Found ${resources.length} resources`); } if (this.importSystems) { this.logger.info(`Found ${systems.length} systems`); } const entities = [...resources, ...systems]; if (entities.length === 0) { this.logger.info("No entities found"); return; } await this.connection.applyMutation({ type: "full", entities: entities.map((entity) => ({ entity, locationKey: "firefly" })) }); this.logger.info(`Firefly refresh completed, ${entities.length} entities found`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error("Failed to refresh Firefly assets", { error: errorMessage }); } } /** * Retrieves all components from the catalog * @returns An array of component identifiers */ async getAllComponents() { try { const components = await this.catalogService.getEntities({ filter: { kind: "Component" }, fields: [ "metadata.name", "metadata.namespace", "metadata.labels" ] }, { credentials: await this.auth.getOwnServiceCredentials() }); return components.items.map((component) => ({ name: component.metadata.name, namespace: component.metadata.namespace || "default", labels: component.metadata.labels || {}, ref: `component:${component.metadata.namespace}/${component.metadata.name}` })); } catch (error) { this.logger.error(`Failed to fetch components: ${error}`); return []; } } /** * Creates system entities from the collected assets * Each cloud provider account/project becomes a system entity * * @param assets - The assets fetched from Firefly * @returns An array of system entities */ getSystems(assets) { const originProviders = {}; assets.forEach((asset) => { originProviders[asset.providerId] = { name: asset.providerId, owner: "Firefly", type: asset.assetType.split("_")[0] // Extract provider type (aws, gcp, azure) }; }); return Object.values(originProviders).map((provider) => ({ apiVersion: "backstage.io/v1alpha1", kind: "System", metadata: { name: provider.name, annotations: { "backstage.io/managed-by-location": `url:https://app.firefly.ai/`, "backstage.io/managed-by-origin-location": `url:https://app.firefly.ai/`, // Resource managed by Firefly identifier "firefly.ai/managed-by-firefly": "true", // Provider information "firefly.ai/origin-provider-id": provider.name } }, spec: { owner: "firefly", type: provider.type } })); } /** * Converts a Firefly asset to a Backstage entity * Maps asset metadata to entity annotations and establishes relationships * * @param asset - The Firefly asset to convert * @param components - The components from the catalog * @returns A Backstage entity representing the asset */ async assetToEntity(asset, components) { const assetIdHash = crypto__default.default.createHash("sha1").update(asset.fireflyAssetId).digest("hex"); const connectionSourcesIds = asset.connectionSources.map( (source) => `resource:${crypto__default.default.createHash("sha1").update(source).digest("hex")}` ); const connectionTargetsIds = asset.connectionTargets.map( (target) => `resource:${crypto__default.default.createHash("sha1").update(target).digest("hex")}` ); const labels = this.getLabels(asset.tagsList); labels.location = asset.region || "unknown"; let relatedComponentsRefs = []; if (this.tagKeysIdentifiers.length > 0) { const labelsKeys = Object.keys(labels); const allTagsPresent = this.tagKeysIdentifiers.every((tag) => labelsKeys.includes(tag)); if (allTagsPresent) { const tagsKeyValues = this.tagKeysIdentifiers.reduce((acc, tag) => { acc[tag] = labels[tag]; return acc; }, {}); for (const component of components) { const componentLabels = component.labels; const isRelated = Object.entries(tagsKeyValues).every(([key, value]) => componentLabels[key] === value); if (isRelated) { relatedComponentsRefs.push(component.ref); } } } } let tags = Object.values(labels).map( (value) => value.toLowerCase().replace(/[^a-z0-9+#\-]/g, "-").substring(0, 63) ).filter((tag) => tag.length >= 1); tags = tags.filter( (value, index, self) => self.indexOf(value) === index ); const tagsWithoutLocation = tags.filter((tag) => tag !== labels.location); if (this.correlateByComponentName && tagsWithoutLocation.length > 0) { for (const component of components) { const componentName = component.name; if (tagsWithoutLocation.includes(componentName)) { relatedComponentsRefs.push(component.ref); } } } relatedComponentsRefs = relatedComponentsRefs.filter( (value, index, self) => self.indexOf(value) === index ); return { apiVersion: "backstage.io/v1alpha1", kind: "Resource", metadata: { labels, tags, name: assetIdHash, title: asset.name, description: JSON.stringify(asset.tfObject), annotations: { "backstage.io/managed-by-location": `url:${asset.fireflyLink}`, "backstage.io/managed-by-origin-location": `url:${asset.fireflyLink}`, // Resource managed by Firefly identifier "firefly.ai/managed-by-firefly": "true", // Asset identification annotations "firefly.ai/asset-id": asset.assetId, "firefly.ai/resource-id": asset.resourceId, "firefly.ai/fireflyAssetId": asset.fireflyAssetId, // Resource metadata annotations "firefly.ai/name": asset.name, "firefly.ai/arn": asset.arn, "firefly.ai/state": asset.state, "firefly.ai/location": asset.region, "firefly.ai/owner": asset.owner, // Links to external resources "firefly.ai/cloud-link": asset.consoleURL, "firefly.ai/code-link": asset.vcsCodeLink, "firefly.ai/firefly-link": asset.fireflyLink, // Infrastructure as code information "firefly.ai/iac-type": asset.iacType, "firefly.ai/terraform-module": asset.terraformModule, "firefly.ai/terraform-object-name": asset.terraformObjectName, "firefly.ai/delete-command": asset.deleteCommand, "firefly.ai/state-location": asset.stateLocationString, // Provider information "firefly.ai/origin-provider-id": asset.providerId, "firefly.ai/vcs-provider": asset.vcsProvider, "firefly.ai/vcs-repo": asset.vcsRepo, // Timestamps "firefly.ai/resource-creation-date": String(asset.resourceCreationDate), "firefly.ai/last-resource-state-change": String(asset.lastResourceStateChange), // Configuration data "firefly.ai/asset-config": JSON.stringify(asset.tfObject) }, links: [ ...asset.consoleURL ? [{ url: asset.consoleURL, title: "Cloud Link" }] : [], ...asset.vcsCodeLink ? [{ url: asset.vcsCodeLink, title: "Code Link" }] : [], ...asset.fireflyLink ? [{ url: asset.fireflyLink, title: "Firefly Link" }] : [] ] }, spec: { type: asset.assetType || "unknown", owner: asset.owner || "unknown", system: asset.providerId || "unknown", lifecycle: asset.state || "unknown", dependsOn: connectionSourcesIds || [], dependencyOf: [...connectionTargetsIds, ...relatedComponentsRefs] } }; } /** * Sanitizes a name to make it valid for Entity labels/annotations * * @param name - The string to sanitize * @returns A sanitized string that conforms to Entity label constraints */ validName(name) { let updatedName = name.replace(/[^a-zA-Z0-9\-_.]/g, "_").substring(0, 63); updatedName = updatedName.replace(/[-_.]{2,}/g, "_").replace(/^[-_.]|[-_.]$/g, ""); return updatedName; } /** * Converts Firefly tags to Entity-compatible labels * * @param tagsList - List of tags from Firefly * @returns A record of key-value pairs conforming to Entity label format */ getLabels(tagsList) { return tagsList.reduce((acc, tag) => { const parts = tag.split(": "); let key = parts[0] || ""; key = key.toLowerCase(); let value = parts[1] || ""; key = this.validName(key); value = this.validName(value); acc[key] = value; return acc; }, {}); } } exports.FireflyEntityProvider = FireflyEntityProvider; //# sourceMappingURL=fireflyEntityProvider.cjs.js.map