Skip to main content

04. Plugin Development Guide

Plugin Architecture Overview

Backstage Plugin Types

Plugin Components

ComponentLocationPurpose
Frontend Pluginplugins/{plugin-name}React UI components and pages
Backend Pluginplugins/{plugin-name}-backendAPI endpoints and business logic
Commonplugins/{plugin-name}-commonShared types and utilities
Node Libraryplugins/{plugin-name}-nodeBackend utilities and clients

Development Environment Setup

Create Plugin Workspace

# Create frontend plugin
yarn backstage-cli create-plugin \
--plugin-id deployment \
--owner @company/platform-team

# Create backend plugin
yarn backstage-cli create-plugin \
--plugin-id deployment \
--backend \
--owner @company/platform-team

# Create common package
mkdir -p plugins/deployment-common
cd plugins/deployment-common
yarn init

Plugin Structure

plugins/
├── deployment/ # Frontend plugin
│ ├── src/
│ │ ├── index.ts # Plugin entry
│ │ ├── plugin.ts # Plugin definition
│ │ ├── routes.ts # Route definitions
│ │ ├── api/
│ │ │ ├── DeploymentApi.ts # API interface
│ │ │ └── DeploymentClient.ts # API implementation
│ │ ├── components/
│ │ │ ├── DeploymentPage/
│ │ │ ├── DeploymentHistory/
│ │ │ ├── DeploymentForm/
│ │ │ └── DeploymentStatus/
│ │ └── hooks/
│ │ ├── useDeployments.ts
│ │ └── useWorkflowStatus.ts
│ ├── dev/
│ │ └── home.tsx # Development setup
│ └── package.json

├── deployment-backend/ # Backend plugin
│ ├── src/
│ │ ├── plugin.ts # Plugin entry
│ │ ├── service/
│ │ │ ├── router.ts # Express router
│ │ │ ├── DeploymentService.ts # Business logic
│ │ │ └── WorkflowClient.ts # Argo Workflows client
│ │ └── types.ts
│ └── package.json

└── deployment-common/ # Shared code
├── src/
│ ├── types.ts # Shared types
│ ├── constants.ts
│ └── validators.ts
└── package.json

Deployment Plugin

Frontend Plugin

Plugin Definition

// plugins/deployment/src/plugin.ts
import {
createPlugin,
createRoutableExtension,
createApiFactory,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { DeploymentClient } from './api/DeploymentClient';
import { deploymentApiRef } from './api/DeploymentApi';

export const deploymentPlugin = createPlugin({
id: 'deployment',
apis: [
createApiFactory({
api: deploymentApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, identityApi }) =>
new DeploymentClient({ discoveryApi, identityApi }),
}),
],
routes: {
root: rootRouteRef,
},
});

export const DeploymentPage = deploymentPlugin.provide(
createRoutableExtension({
name: 'DeploymentPage',
component: () =>
import('./components/DeploymentPage').then(m => m.DeploymentPage),
mountPoint: rootRouteRef,
}),
);

API Client

// plugins/deployment/src/api/DeploymentClient.ts
import {
DiscoveryApi,
IdentityApi,
createApiRef,
} from '@backstage/core-plugin-api';
import {
DeploymentRequest,
DeploymentStatus,
DeploymentHistory,
} from '@internal/plugin-deployment-common';

export const deploymentApiRef = createApiRef<DeploymentApi>({
id: 'plugin.deployment.api',
});

export interface DeploymentApi {
triggerDeployment(request: DeploymentRequest): Promise<DeploymentStatus>;
getDeploymentStatus(deploymentId: string): Promise<DeploymentStatus>;
getDeploymentHistory(
entityRef: string,
environment: string,
): Promise<DeploymentHistory[]>;
rollback(deploymentId: string): Promise<DeploymentStatus>;
}

export class DeploymentClient implements DeploymentApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;

constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
}

private async getBaseUrl(): Promise<string> {
return await this.discoveryApi.getBaseUrl('deployment');
}

private async getHeaders(): Promise<HeadersInit> {
const { token } = await this.identityApi.getCredentials();
return {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
};
}

async triggerDeployment(
request: DeploymentRequest,
): Promise<DeploymentStatus> {
const baseUrl = await this.getBaseUrl();
const headers = await this.getHeaders();

const response = await fetch(`${baseUrl}/deployments`, {
method: 'POST',
headers,
body: JSON.stringify(request),
});

if (!response.ok) {
throw new Error(
`Failed to trigger deployment: ${response.statusText}`,
);
}

return await response.json();
}

async getDeploymentStatus(
deploymentId: string,
): Promise<DeploymentStatus> {
const baseUrl = await this.getBaseUrl();
const headers = await this.getHeaders();

const response = await fetch(
`${baseUrl}/deployments/${deploymentId}`,
{ headers },
);

if (!response.ok) {
throw new Error(
`Failed to get deployment status: ${response.statusText}`,
);
}

return await response.json();
}

async getDeploymentHistory(
entityRef: string,
environment: string,
): Promise<DeploymentHistory[]> {
const baseUrl = await this.getBaseUrl();
const headers = await this.getHeaders();

const params = new URLSearchParams({
entityRef,
environment,
});

const response = await fetch(`${baseUrl}/history?${params}`, {
headers,
});

if (!response.ok) {
throw new Error(
`Failed to get deployment history: ${response.statusText}`,
);
}

return await response.json();
}

async rollback(deploymentId: string): Promise<DeploymentStatus> {
const baseUrl = await this.getBaseUrl();
const headers = await this.getHeaders();

const response = await fetch(
`${baseUrl}/deployments/${deploymentId}/rollback`,
{
method: 'POST',
headers,
},
);

if (!response.ok) {
throw new Error(`Failed to rollback: ${response.statusText}`);
}

return await response.json();
}
}

Deployment Form Component

// plugins/deployment/src/components/DeploymentForm/DeploymentForm.tsx
import React, { useState } from 'react';
import {
Box,
Button,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
Card,
CardContent,
CardHeader,
Alert,
} from '@material-ui/core';
import { useApi } from '@backstage/core-plugin-api';
import { deploymentApiRef } from '../../api/DeploymentApi';
import { useEntity } from '@backstage/plugin-catalog-react';
import { DeploymentRequest } from '@internal/plugin-deployment-common';

export const DeploymentForm = () => {
const { entity } = useEntity();
const deploymentApi = useApi(deploymentApiRef);

const [environment, setEnvironment] = useState('dev');
const [strategy, setStrategy] = useState('standard');
const [version, setVersion] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setLoading(true);
setError(null);
setSuccess(null);

try {
const request: DeploymentRequest = {
entityRef: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`,
environment,
strategy,
version: version || 'latest',
parameters: {},
};

const result = await deploymentApi.triggerDeployment(request);
setSuccess(
`Deployment triggered successfully! Workflow ID: ${result.workflowId}`,
);
setVersion('');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};

return (
<Card>
<CardHeader title="Deploy Application" />
<CardContent>
<form onSubmit={handleSubmit}>
<Box display="flex" flexDirection="column" gap={2}>
<FormControl fullWidth>
<InputLabel>Environment</InputLabel>
<Select
value={environment}
onChange={e => setEnvironment(e.target.value as string)}
required
>
<MenuItem value="dev">Development</MenuItem>
<MenuItem value="staging">Staging</MenuItem>
<MenuItem value="production">Production</MenuItem>
</Select>
</FormControl>

<FormControl fullWidth>
<InputLabel>Deployment Strategy</InputLabel>
<Select
value={strategy}
onChange={e => setStrategy(e.target.value as string)}
required
>
<MenuItem value="standard">Standard (Rolling)</MenuItem>
<MenuItem value="blue-green">Blue/Green</MenuItem>
<MenuItem value="canary">Canary</MenuItem>
</Select>
</FormControl>

<TextField
label="Version (optional)"
placeholder="e.g., v1.2.3 or latest"
value={version}
onChange={e => setVersion(e.target.value)}
fullWidth
helperText="Leave empty to deploy latest version"
/>

{error && <Alert severity="error">{error}</Alert>}
{success && <Alert severity="success">{success}</Alert>}

<Button
type="submit"
variant="contained"
color="primary"
disabled={loading}
>
{loading ? 'Deploying...' : 'Deploy'}
</Button>
</Box>
</form>
</CardContent>
</Card>
);
};

Deployment History Component

// plugins/deployment/src/components/DeploymentHistory/DeploymentHistory.tsx
import React from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
IconButton,
Tooltip,
} from '@material-ui/core';
import { Refresh as RefreshIcon, Undo as UndoIcon } from '@material-ui/icons';
import { useDeploymentHistory } from '../../hooks/useDeployments';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Progress } from '@backstage/core-components';

export const DeploymentHistory = ({ environment }: { environment: string }) => {
const { entity } = useEntity();
const { history, loading, error, refetch, rollback } = useDeploymentHistory(
`${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`,
environment,
);

if (loading) return <Progress />;
if (error) return <div>Error: {error.message}</div>;

const getStatusColor = (status: string) => {
switch (status) {
case 'succeeded':
return 'success';
case 'failed':
return 'error';
case 'running':
return 'primary';
default:
return 'default';
}
};

return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Timestamp</TableCell>
<TableCell>Version</TableCell>
<TableCell>Strategy</TableCell>
<TableCell>Status</TableCell>
<TableCell>Deployed By</TableCell>
<TableCell>Duration</TableCell>
<TableCell>
Actions
<IconButton size="small" onClick={refetch}>
<RefreshIcon />
</IconButton>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{history.map(deployment => (
<TableRow key={deployment.id}>
<TableCell>
{new Date(deployment.timestamp).toLocaleString()}
</TableCell>
<TableCell>{deployment.version}</TableCell>
<TableCell>{deployment.strategy}</TableCell>
<TableCell>
<Chip
label={deployment.status}
color={getStatusColor(deployment.status) as any}
size="small"
/>
</TableCell>
<TableCell>{deployment.deployedBy}</TableCell>
<TableCell>{deployment.duration}</TableCell>
<TableCell>
{deployment.status === 'succeeded' && (
<Tooltip title="Rollback to this version">
<IconButton
size="small"
onClick={() => rollback(deployment.id)}
>
<UndoIcon />
</IconButton>
</Tooltip>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

Backend Plugin

Router Setup

// plugins/deployment-backend/src/service/router.ts
import { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { DeploymentService } from './DeploymentService';

export interface RouterOptions {
logger: Logger;
config: Config;
}

export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, config } = options;

const deploymentService = new DeploymentService(logger, config);

const router = Router();
router.use(express.json());

// Trigger deployment
router.post('/deployments', async (req, res) => {
const { entityRef, environment, strategy, version, parameters } = req.body;

logger.info(`Triggering deployment for ${entityRef} to ${environment}`);

try {
const result = await deploymentService.triggerDeployment({
entityRef,
environment,
strategy,
version,
parameters,
});

res.json(result);
} catch (error) {
logger.error('Failed to trigger deployment', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Get deployment status
router.get('/deployments/:deploymentId', async (req, res) => {
const { deploymentId } = req.params;

try {
const status = await deploymentService.getDeploymentStatus(deploymentId);
res.json(status);
} catch (error) {
logger.error('Failed to get deployment status', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Get deployment history
router.get('/history', async (req, res) => {
const { entityRef, environment } = req.query;

if (!entityRef || !environment) {
res.status(400).json({ error: 'Missing required parameters' });
return;
}

try {
const history = await deploymentService.getDeploymentHistory(
entityRef as string,
environment as string,
);
res.json(history);
} catch (error) {
logger.error('Failed to get deployment history', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Rollback deployment
router.post('/deployments/:deploymentId/rollback', async (req, res) => {
const { deploymentId } = req.params;

try {
const result = await deploymentService.rollback(deploymentId);
res.json(result);
} catch (error) {
logger.error('Failed to rollback deployment', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});

router.use(errorHandler());
return router;
}

Deployment Service

// plugins/deployment-backend/src/service/DeploymentService.ts
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { WorkflowClient } from './WorkflowClient';
import {
DeploymentRequest,
DeploymentStatus,
DeploymentHistory,
} from '@internal/plugin-deployment-common';

export class DeploymentService {
private workflowClient: WorkflowClient;
private logger: Logger;

constructor(logger: Logger, config: Config) {
this.logger = logger;
this.workflowClient = new WorkflowClient(
config.getString('argoWorkflows.baseUrl'),
config.getString('argoWorkflows.token'),
);
}

async triggerDeployment(
request: DeploymentRequest,
): Promise<DeploymentStatus> {
this.logger.info(
`Triggering ${request.strategy} deployment for ${request.entityRef}`,
);

// Determine workflow template based on strategy
const templateName = this.getWorkflowTemplate(request.strategy);

// Parse entity reference
const [kind, namespaceAndName] = request.entityRef.split(':');
const [namespace, name] = namespaceAndName.split('/');

// Prepare workflow parameters
const parameters = {
'app-name': name,
'environment': request.environment,
'version': request.version,
'strategy': request.strategy,
...request.parameters,
};

// Submit workflow
const workflow = await this.workflowClient.submitWorkflow(
templateName,
parameters,
);

return {
deploymentId: workflow.metadata.name,
workflowId: workflow.metadata.name,
status: workflow.status.phase,
entityRef: request.entityRef,
environment: request.environment,
version: request.version,
strategy: request.strategy,
timestamp: workflow.metadata.creationTimestamp,
};
}

async getDeploymentStatus(
deploymentId: string,
): Promise<DeploymentStatus> {
const workflow = await this.workflowClient.getWorkflow(deploymentId);

return {
deploymentId: workflow.metadata.name,
workflowId: workflow.metadata.name,
status: workflow.status.phase,
entityRef: workflow.metadata.labels['backstage.io/entity-ref'],
environment: workflow.metadata.labels.environment,
version: workflow.metadata.labels.version,
strategy: workflow.metadata.labels.strategy,
timestamp: workflow.metadata.creationTimestamp,
completedAt: workflow.status.finishedAt,
};
}

async getDeploymentHistory(
entityRef: string,
environment: string,
): Promise<DeploymentHistory[]> {
const labelSelector = `backstage.io/entity-ref=${entityRef},environment=${environment}`;

const workflows = await this.workflowClient.listWorkflows(labelSelector);

return workflows.items
.map(workflow => ({
id: workflow.metadata.name,
entityRef,
environment,
version: workflow.metadata.labels.version,
strategy: workflow.metadata.labels.strategy,
status: workflow.status.phase,
timestamp: workflow.metadata.creationTimestamp,
completedAt: workflow.status.finishedAt,
duration: this.calculateDuration(
workflow.metadata.creationTimestamp,
workflow.status.finishedAt,
),
deployedBy: workflow.metadata.labels['deployed-by'],
}))
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
);
}

async rollback(deploymentId: string): Promise<DeploymentStatus> {
// Get original deployment
const originalDeployment = await this.getDeploymentStatus(deploymentId);

// Trigger rollback workflow
const rollbackRequest: DeploymentRequest = {
entityRef: originalDeployment.entityRef,
environment: originalDeployment.environment,
strategy: 'rollback',
version: originalDeployment.version,
parameters: {
'rollback-to': deploymentId,
},
};

return this.triggerDeployment(rollbackRequest);
}

private getWorkflowTemplate(strategy: string): string {
const templates: Record<string, string> = {
standard: 'deployment-standard',
'blue-green': 'deployment-blue-green',
canary: 'deployment-canary',
rollback: 'rollback-instant',
};

return templates[strategy] || 'deployment-standard';
}

private calculateDuration(start: string, end?: string): string {
if (!end) return 'In progress';

const duration = new Date(end).getTime() - new Date(start).getTime();
const minutes = Math.floor(duration / 60000);
const seconds = Math.floor((duration % 60000) / 1000);

return `${minutes}m ${seconds}s`;
}
}

Workflow Client

// plugins/deployment-backend/src/service/WorkflowClient.ts
import fetch from 'node-fetch';

interface WorkflowSubmission {
metadata: {
name: string;
labels: Record<string, string>;
creationTimestamp: string;
};
status: {
phase: string;
finishedAt?: string;
};
}

export class WorkflowClient {
private baseUrl: string;
private token: string;

constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl;
this.token = token;
}

async submitWorkflow(
templateName: string,
parameters: Record<string, string>,
): Promise<WorkflowSubmission> {
const workflow = {
apiVersion: 'argoproj.io/v1alpha1',
kind: 'Workflow',
metadata: {
generateName: `${templateName}-`,
labels: parameters,
},
spec: {
workflowTemplateRef: {
name: templateName,
},
arguments: {
parameters: Object.entries(parameters).map(([name, value]) => ({
name,
value,
})),
},
},
};

const response = await fetch(`${this.baseUrl}/api/v1/workflows/argo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify(workflow),
});

if (!response.ok) {
throw new Error(`Failed to submit workflow: ${response.statusText}`);
}

return await response.json();
}

async getWorkflow(name: string): Promise<WorkflowSubmission> {
const response = await fetch(
`${this.baseUrl}/api/v1/workflows/argo/${name}`,
{
headers: {
Authorization: `Bearer ${this.token}`,
},
},
);

if (!response.ok) {
throw new Error(`Failed to get workflow: ${response.statusText}`);
}

return await response.json();
}

async listWorkflows(
labelSelector: string,
): Promise<{ items: WorkflowSubmission[] }> {
const params = new URLSearchParams({
listOptions: JSON.stringify({
labelSelector,
}),
});

const response = await fetch(
`${this.baseUrl}/api/v1/workflows/argo?${params}`,
{
headers: {
Authorization: `Bearer ${this.token}`,
},
},
);

if (!response.ok) {
throw new Error(`Failed to list workflows: ${response.statusText}`);
}

return await response.json();
}
}

Common Types

// plugins/deployment-common/src/types.ts
export interface DeploymentRequest {
entityRef: string;
environment: string;
strategy: 'standard' | 'blue-green' | 'canary' | 'rollback';
version: string;
parameters?: Record<string, string>;
}

export interface DeploymentStatus {
deploymentId: string;
workflowId: string;
status: 'pending' | 'running' | 'succeeded' | 'failed';
entityRef: string;
environment: string;
version: string;
strategy: string;
timestamp: string;
completedAt?: string;
error?: string;
}

export interface DeploymentHistory {
id: string;
entityRef: string;
environment: string;
version: string;
strategy: string;
status: string;
timestamp: string;
completedAt?: string;
duration: string;
deployedBy: string;
}

Traffic Management Plugin

Plugin Overview

The Traffic Management plugin provides UI controls for managing traffic routing between different versions of services.

Frontend Component

// plugins/traffic-management/src/components/TrafficSplitControl.tsx
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
CardHeader,
Slider,
Button,
Typography,
Grid,
} from '@material-ui/core';
import { useApi } from '@backstage/core-plugin-api';
import { trafficManagementApiRef } from '../api/TrafficManagementApi';

export const TrafficSplitControl = ({
serviceName,
environment,
}: {
serviceName: string;
environment: string;
}) => {
const trafficApi = useApi(trafficManagementApiRef);
const [blueWeight, setBlueWeight] = useState(100);
const [applying, setApplying] = useState(false);

const greenWeight = 100 - blueWeight;

const handleApply = async () => {
setApplying(true);
try {
await trafficApi.updateTrafficSplit(serviceName, environment, {
blue: blueWeight,
green: greenWeight,
});
} catch (error) {
console.error('Failed to update traffic split', error);
} finally {
setApplying(false);
}
};

return (
<Card>
<CardHeader title="Traffic Split Control" />
<CardContent>
<Box p={2}>
<Grid container spacing={3}>
<Grid item xs={6}>
<Box
p={2}
bgcolor="primary.light"
borderRadius={4}
textAlign="center"
>
<Typography variant="h6">Blue</Typography>
<Typography variant="h3">{blueWeight}%</Typography>
</Box>
</Grid>
<Grid item xs={6}>
<Box
p={2}
bgcolor="success.light"
borderRadius={4}
textAlign="center"
>
<Typography variant="h6">Green</Typography>
<Typography variant="h3">{greenWeight}%</Typography>
</Box>
</Grid>
</Grid>

<Box mt={4}>
<Slider
value={blueWeight}
onChange={(_, value) => setBlueWeight(value as number)}
min={0}
max={100}
step={5}
marks={[
{ value: 0, label: '0%' },
{ value: 50, label: '50%' },
{ value: 100, label: '100%' },
]}
valueLabelDisplay="auto"
/>
</Box>

<Box mt={3}>
<Button
variant="contained"
color="primary"
fullWidth
onClick={handleApply}
disabled={applying}
>
{applying ? 'Applying...' : 'Apply Traffic Split'}
</Button>
</Box>
</Box>
</CardContent>
</Card>
);
};

Backend Service

// plugins/traffic-management-backend/src/service/TrafficManagementService.ts
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import * as k8s from '@kubernetes/client-node';

export class TrafficManagementService {
private k8sApi: k8s.CustomObjectsApi;
private logger: Logger;

constructor(logger: Logger, config: Config) {
this.logger = logger;

const kc = new k8s.KubeConfig();
kc.loadFromDefault();
this.k8sApi = kc.makeApiClient(k8s.CustomObjectsApi);
}

async updateTrafficSplit(
serviceName: string,
environment: string,
weights: { blue: number; green: number },
): Promise<void> {
this.logger.info(
`Updating traffic split for ${serviceName} in ${environment}`,
);

// Update VirtualService (Istio)
const virtualService = {
apiVersion: 'networking.istio.io/v1beta1',
kind: 'VirtualService',
metadata: {
name: serviceName,
namespace: environment,
},
spec: {
hosts: [`${serviceName}.${environment}.svc.cluster.local`],
http: [
{
route: [
{
destination: {
host: `${serviceName}-blue`,
port: { number: 80 },
},
weight: weights.blue,
},
{
destination: {
host: `${serviceName}-green`,
port: { number: 80 },
},
weight: weights.green,
},
],
},
],
},
};

await this.k8sApi.patchNamespacedCustomObject(
'networking.istio.io',
'v1beta1',
environment,
'virtualservices',
serviceName,
virtualService,
undefined,
undefined,
undefined,
{ headers: { 'Content-Type': 'application/merge-patch+json' } },
);

this.logger.info('Traffic split updated successfully');
}

async getTrafficSplit(
serviceName: string,
environment: string,
): Promise<{ blue: number; green: number }> {
const response = await this.k8sApi.getNamespacedCustomObject(
'networking.istio.io',
'v1beta1',
environment,
'virtualservices',
serviceName,
);

const virtualService = response.body as any;
const routes = virtualService.spec.http[0].route;

return {
blue: routes.find((r: any) => r.destination.host.includes('blue'))
?.weight || 0,
green: routes.find((r: any) => r.destination.host.includes('green'))
?.weight || 0,
};
}
}

Multi-Cluster Monitoring Plugin

Cluster Status Dashboard

// plugins/multi-cluster/src/components/ClusterStatusDashboard.tsx
import React from 'react';
import { Grid, Card, CardContent, Typography, Chip } from '@material-ui/core';
import { useClusterStatus } from '../hooks/useClusterStatus';
import { Progress } from '@backstage/core-components';

export const ClusterStatusDashboard = () => {
const { clusters, loading } = useClusterStatus();

if (loading) return <Progress />;

return (
<Grid container spacing={3}>
{clusters.map(cluster => (
<Grid item xs={12} md={4} key={cluster.name}>
<Card>
<CardContent>
<Typography variant="h6">{cluster.name}</Typography>
<Typography color="textSecondary">{cluster.region}</Typography>

<Box mt={2}>
<Chip
label={cluster.status}
color={cluster.status === 'healthy' ? 'success' : 'error'}
/>
</Box>

<Box mt={2}>
<Typography variant="body2">
Nodes: {cluster.nodes.ready}/{cluster.nodes.total}
</Typography>
<Typography variant="body2">
CPU: {cluster.resources.cpuUsage}%
</Typography>
<Typography variant="body2">
Memory: {cluster.resources.memoryUsage}%
</Typography>
</Box>

<Box mt={2}>
<Typography variant="caption">
Deployments: {cluster.deployments}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};

Testing Strategies

Unit Tests

// plugins/deployment/src/api/DeploymentClient.test.ts
import { DeploymentClient } from './DeploymentClient';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';

describe('DeploymentClient', () => {
let client: DeploymentClient;
let mockDiscoveryApi: jest.Mocked<DiscoveryApi>;
let mockIdentityApi: jest.Mocked<IdentityApi>;

beforeEach(() => {
mockDiscoveryApi = {
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/api/deployment'),
} as any;

mockIdentityApi = {
getCredentials: jest.fn().mockResolvedValue({ token: 'test-token' }),
} as any;

client = new DeploymentClient({
discoveryApi: mockDiscoveryApi,
identityApi: mockIdentityApi,
});

global.fetch = jest.fn();
});

it('should trigger deployment', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
deploymentId: 'test-123',
status: 'running',
}),
};

(global.fetch as jest.Mock).mockResolvedValue(mockResponse);

const result = await client.triggerDeployment({
entityRef: 'component:default/test-app',
environment: 'dev',
strategy: 'standard',
version: 'v1.0.0',
});

expect(result.deploymentId).toBe('test-123');
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:7007/api/deployment/deployments',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
}),
);
});
});

Integration Tests

// plugins/deployment-backend/src/service/DeploymentService.test.ts
import { DeploymentService } from './DeploymentService';
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';

describe('DeploymentService', () => {
let service: DeploymentService;

beforeEach(() => {
const config = new ConfigReader({
argoWorkflows: {
baseUrl: 'http://localhost:2746',
token: 'test-token',
},
});

service = new DeploymentService(getVoidLogger(), config);
});

it('should trigger standard deployment', async () => {
const request = {
entityRef: 'component:default/test-app',
environment: 'dev',
strategy: 'standard' as const,
version: 'v1.0.0',
};

const result = await service.triggerDeployment(request);

expect(result.status).toBeDefined();
expect(result.deploymentId).toBeDefined();
});
});

Publishing and Distribution

NPM Publishing

// package.json
{
"name": "@company/backstage-plugin-deployment",
"version": "1.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "restricted",
"registry": "https://npm.company.com"
}
}

Internal Plugin Registry

# Publish to internal registry
yarn workspace @company/backstage-plugin-deployment publish

# Install in other Backstage instances
yarn add @company/backstage-plugin-deployment

Best Practices

1. Code Organization

  • ✅ Separate frontend, backend, and common code
  • ✅ Use TypeScript for type safety
  • ✅ Follow Backstage naming conventions
  • ✅ Keep components small and focused

2. API Design

  • ✅ Use RESTful endpoints
  • ✅ Implement proper error handling
  • ✅ Add request validation
  • ✅ Include authentication/authorization

3. Testing

  • ✅ Write unit tests for all services
  • ✅ Add integration tests for API endpoints
  • ✅ Test error scenarios
  • ✅ Use mocks for external dependencies

4. Performance

  • ✅ Implement caching where appropriate
  • ✅ Use pagination for large datasets
  • ✅ Optimize database queries
  • ✅ Lazy load components

5. Security

  • ✅ Validate all inputs
  • ✅ Use parameterized queries
  • ✅ Implement RBAC
  • ✅ Audit logging for sensitive operations