@fireflyai/backstage-backend-plugin-firefly
Version:
Firefly backend plugin for Backstage
318 lines (312 loc) • 11.8 kB
JavaScript
;
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