Skip to content

Backstage

Ops-Catalog can be used from right within Backstage by adding a Catalog mapping plugin like so:

Create plugin with CatalogBuilder

path: packages/backend/src/plugins/catalog.ts

import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { OpsProvider } from './OpsProvider';
import { OpsProcessor } from './OpsProcessor';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<any> {
  const builder = CatalogBuilder.create(env);
  const frobs = new OpsProvider(env);
  builder.addEntityProvider(frobs);
  builder.addProcessor(new OpsProcessor());
  const { processingEngine, router } = await builder.build();
  await processingEngine.start();
  await env.scheduler.scheduleTask({
    id: 'run_ops_refresh',
    fn: async () => {
      await frobs.run()
    },
    frequency: { minutes: 2 },
    timeout: { minutes: 1 },
  });
  return router;
}

Create Processor

path: packages/backend/src/plugins/OpsProcessor.ts

import {
    Entity,
  } from '@backstage/catalog-model';
  import {
    CatalogProcessor,
    CatalogProcessorEmit,
  } from '@backstage/plugin-catalog-node';
  import { LocationSpec } from '@backstage/plugin-catalog-common';


  export class OpsProcessor implements CatalogProcessor {
    getProcessorName(): string {
      return 'OpsProcessor';
    }

    private readonly validators = [];

    async validateEntityKind(entity: Entity): Promise<boolean> {
      return entity.apiVersion === "v1";
    }

    async postProcessEntity(
      entity: Entity,
      _location: LocationSpec,
      emit: CatalogProcessorEmit,
    ): Promise<Entity> {

      return entity;
    }
  }

Create Mapping

path: packages/backend/src/plugins/OpsProvider.ts

import {
  ANNOTATION_LOCATION,
  ANNOTATION_ORIGIN_LOCATION,
} from '@backstage/catalog-model';
import {
  EntityProvider,
  EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { PluginEnvironment } from '../types';

export class OpsProvider implements EntityProvider {
  private connection?: EntityProviderConnection;
  private env: PluginEnvironment;

  public constructor(env: PluginEnvironment) {
    this.env = env;
  }
  getProviderName(): string {
    return `ops-catalog`;
  }

  public async connect(connection: EntityProviderConnection): Promise<void> {
    this.connection = connection;
  }

  async run(): Promise<void> {
    if (!this.connection) {
      throw new Error('DB not initilized');
    }
    const catalogObj: any[] = [];
    const catalogResponse = await fetch('http://localhost:8080/api/catalog');
    const d = await catalogResponse.json();

    for (const entity of d.data) {
      let linkSource = entity.links || [];
      const links = linkSource.map((link:{url:string, type:string}) => {
        return {
          url: link.url,
          title: link.type
        }
      });
      let apiVersion = 'backstage.io/v1alpha1';
      let kind = entity.kind;
      switch(kind) {
        case 'Resource':
          break;
        case 'Component':
          break;
        default:
          apiVersion = entity.apiVersion;
      }
      let dependenciesSource = entity.dependencies || { downstream: [], upstream: []};
      let dependencies = (
        dependenciesSource.downstream || []).concat(dependenciesSource.upstream || []
        );

      let name = `${entity.metadata.name.replaceAll(" ", "")}`;
      let owner = '@backstage/maintainers';
      if (entity.contact.owner) {
        owner = `${entity.contact.owner.id}`; 
      }
      let annotations = entity.metadata.annotations || {};
      annotations[ANNOTATION_LOCATION] ='ops:https://ops-catalog.io/';
      annotations[ANNOTATION_ORIGIN_LOCATION]='ops:https://ops-catalog.io/';

      let tags = entity.classification.tag || [];
      let sourceLabels = entity.metadata.labels || {};
      let labels = new Map(Object.entries(sourceLabels)
      .map(([k,v]) => [k,String(v).replaceAll(" ", "").replaceAll("@", "_at_")]))
      let system = entity.classification.capability || "unknown";
      const catalogEntity = {
        kind: kind,
        apiVersion: apiVersion,
        metadata: {
          description: `${entity.metadata.description}`,
          annotations: annotations,
          labels: labels,
          links,
          // name of the entity
          name: `${name}`,
          title: `${name}`,
          tags: tags,
        },
        spec: {
          type: `${entity.class}`,
          lifecycle: 'production',
          owner: owner,
          profile: {
            displayName: `${entity.metadata.name}`,
            email: '',
            picture: '',
          },
          dependsOn: dependencies,
          memberOf: [],
          system: `${system}`
        },
      };

      catalogObj.push(catalogEntity);
    }

    await this.connection?.applyMutation({
      type: 'full',
      entities: catalogObj.map(entity => {
        return { entity, locationKey: 'ops:https://ops-catalog.io/' };
      }),
    });
  }
}

UI

This will show new Kinds in backstage and the relationship defined in ops-catalog will be visible there too.

Component View

Backstage Components

Resource View

Backstage Resources

Endpoint View

Backstage Endpoints