feat: EC2 UI polished + storage persisting
This commit is contained in:
@@ -2,10 +2,19 @@ import {
|
||||
CreateEventBusCommand,
|
||||
DeleteEventBusCommand,
|
||||
DescribeEventBusCommand,
|
||||
DeleteRuleCommand,
|
||||
ListEventBusesCommand,
|
||||
ListRulesCommand,
|
||||
ListTargetsByRuleCommand,
|
||||
PutRuleCommand,
|
||||
PutTargetsCommand,
|
||||
RemoveTargetsCommand,
|
||||
type EventBridgeClient,
|
||||
type EventBus,
|
||||
type Rule,
|
||||
type Target,
|
||||
} from '@aws-sdk/client-eventbridge'
|
||||
import {AddPermissionCommand, type LambdaClient} from '@aws-sdk/client-lambda'
|
||||
import {awsEventBridgeSchema} from '../cloud-spi/eventbridgeSchema'
|
||||
import {ValidationError} from '../cloud-spi/errors'
|
||||
import type {
|
||||
@@ -13,6 +22,9 @@ import type {
|
||||
CloudServiceAdapter,
|
||||
CloudServiceDescriptorOverride,
|
||||
CreateResourceInput,
|
||||
EventBridgeRuleInput,
|
||||
EventBridgeRuleOverview,
|
||||
EventBridgeRuleTarget,
|
||||
ResourceQuery,
|
||||
ServiceSchema,
|
||||
} from '../cloud-spi/types'
|
||||
@@ -24,7 +36,10 @@ export class AwsEventBridgeAdapter implements CloudServiceAdapter {
|
||||
readonly cloud = 'aws' as const
|
||||
readonly service = 'events' as const
|
||||
|
||||
constructor(private readonly eventbridge: EventBridgeClient) {}
|
||||
constructor(
|
||||
private readonly eventbridge: EventBridgeClient,
|
||||
private readonly lambda: LambdaClient,
|
||||
) {}
|
||||
|
||||
schema(): ServiceSchema {
|
||||
return awsEventBridgeSchema()
|
||||
@@ -67,6 +82,90 @@ export class AwsEventBridgeAdapter implements CloudServiceAdapter {
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.eventbridge.send(new DeleteEventBusCommand({Name: eventBusName(id)}))
|
||||
}
|
||||
|
||||
async listEventBridgeRules(eventBusId: string): Promise<EventBridgeRuleOverview[]> {
|
||||
const busName = eventBusName(eventBusId)
|
||||
const rules: Rule[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const response = await this.eventbridge.send(new ListRulesCommand({
|
||||
...eventBusInput(busName),
|
||||
NextToken: nextToken,
|
||||
}))
|
||||
rules.push(...(response.Rules ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
return Promise.all(rules.filter((rule) => Boolean(rule.Name)).map(async (rule) => {
|
||||
const name = rule.Name as string
|
||||
return ruleOverview(rule, busName, await this.listTargets(name, busName))
|
||||
}))
|
||||
}
|
||||
|
||||
async createEventBridgeRule(eventBusId: string, input: EventBridgeRuleInput): Promise<EventBridgeRuleOverview> {
|
||||
const busName = eventBusName(eventBusId)
|
||||
const name = requiredValue(input.name, 'name')
|
||||
const targetInput = ruleInput(input)
|
||||
const response = await this.eventbridge.send(new PutRuleCommand({
|
||||
Name: name,
|
||||
...eventBusInput(busName),
|
||||
...(targetInput.scheduleExpression ? {ScheduleExpression: targetInput.scheduleExpression} : {}),
|
||||
...(targetInput.eventPattern ? {EventPattern: targetInput.eventPattern} : {}),
|
||||
State: targetInput.state,
|
||||
...(targetInput.description ? {Description: targetInput.description} : {}),
|
||||
}))
|
||||
const arn = response.RuleArn
|
||||
if (!arn) throw new ValidationError(`EventBridge rule ${name} did not return an ARN`)
|
||||
|
||||
await this.eventbridge.send(new PutTargetsCommand({
|
||||
Rule: name,
|
||||
...eventBusInput(busName),
|
||||
Targets: input.targets.map((target) => ({Id: target.id, Arn: target.arn})),
|
||||
}))
|
||||
await Promise.all(input.targets.filter((target) => isLambdaArn(target.arn)).map((target) =>
|
||||
this.lambda.send(new AddPermissionCommand({
|
||||
FunctionName: target.arn,
|
||||
StatementId: permissionStatementId(busName, name, target.id),
|
||||
Action: 'lambda:InvokeFunction',
|
||||
Principal: 'events.amazonaws.com',
|
||||
SourceArn: arn,
|
||||
})),
|
||||
))
|
||||
return {
|
||||
name,
|
||||
busName,
|
||||
busId: `${EVENT_BUS_PREFIX}${busName}`,
|
||||
arn,
|
||||
...targetInput,
|
||||
targets: input.targets,
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEventBridgeRule(eventBusId: string, ruleName: string): Promise<void> {
|
||||
const busName = eventBusName(eventBusId)
|
||||
const name = requiredValue(ruleName, 'ruleName')
|
||||
const targets = await this.listTargets(name, busName)
|
||||
const targetIds = targets.map((target) => target.Id).filter((id): id is string => Boolean(id))
|
||||
if (targetIds.length > 0) {
|
||||
await this.eventbridge.send(new RemoveTargetsCommand({Rule: name, ...eventBusInput(busName), Ids: targetIds}))
|
||||
}
|
||||
await this.eventbridge.send(new DeleteRuleCommand({Name: name, ...eventBusInput(busName)}))
|
||||
}
|
||||
|
||||
private async listTargets(ruleName: string, busName: string): Promise<Target[]> {
|
||||
const targets: Target[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const response = await this.eventbridge.send(new ListTargetsByRuleCommand({
|
||||
Rule: ruleName,
|
||||
...eventBusInput(busName),
|
||||
NextToken: nextToken,
|
||||
}))
|
||||
targets.push(...(response.Targets ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
return targets
|
||||
}
|
||||
}
|
||||
|
||||
function eventBusResource(eventBus: EventBus): CloudResource {
|
||||
@@ -122,3 +221,55 @@ function isNotFound(error: unknown): boolean {
|
||||
const value = error as {name?: string; $metadata?: {httpStatusCode?: number}}
|
||||
return value.name === 'ResourceNotFoundException' || value.$metadata?.httpStatusCode === 404
|
||||
}
|
||||
|
||||
function eventBusInput(busName: string): {EventBusName?: string} {
|
||||
return busName === 'default' ? {} : {EventBusName: busName}
|
||||
}
|
||||
|
||||
function ruleInput(input: EventBridgeRuleInput): Pick<EventBridgeRuleOverview, 'scheduleExpression' | 'eventPattern' | 'state' | 'description'> {
|
||||
const scheduleExpression = stringValue(input.scheduleExpression)
|
||||
const eventPattern = stringValue(input.eventPattern)
|
||||
if (Boolean(scheduleExpression) === Boolean(eventPattern)) {
|
||||
throw new ValidationError('exactly one of scheduleExpression or eventPattern is required')
|
||||
}
|
||||
if (eventPattern) {
|
||||
try {
|
||||
JSON.parse(eventPattern)
|
||||
} catch {
|
||||
throw new ValidationError('eventPattern must be a JSON string')
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(input.targets) || input.targets.length === 0) throw new ValidationError('at least one target is required')
|
||||
const targetsValid = input.targets.every((target) => requiredValue(target.id, 'target.id') && requiredValue(target.arn, 'target.arn'))
|
||||
if (!targetsValid) throw new ValidationError('target id and arn are required')
|
||||
const state = input.state ?? 'ENABLED'
|
||||
if (state !== 'ENABLED' && state !== 'DISABLED') throw new ValidationError('state must be ENABLED or DISABLED')
|
||||
return {
|
||||
...(scheduleExpression ? {scheduleExpression} : {}),
|
||||
...(eventPattern ? {eventPattern} : {}),
|
||||
state,
|
||||
...(stringValue(input.description) ? {description: stringValue(input.description)} : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function ruleOverview(rule: Rule, busName: string, targets: Target[]): EventBridgeRuleOverview {
|
||||
return {
|
||||
name: rule.Name ?? '',
|
||||
busName,
|
||||
busId: `${EVENT_BUS_PREFIX}${busName}`,
|
||||
arn: rule.Arn ?? null,
|
||||
...(rule.ScheduleExpression ? {scheduleExpression: rule.ScheduleExpression} : {}),
|
||||
...(rule.EventPattern ? {eventPattern: rule.EventPattern} : {}),
|
||||
state: rule.State === 'DISABLED' ? 'DISABLED' : 'ENABLED',
|
||||
...(rule.Description ? {description: rule.Description} : {}),
|
||||
targets: targets.flatMap((target) => target.Id && target.Arn ? [{id: target.Id, arn: target.Arn}] : []),
|
||||
}
|
||||
}
|
||||
|
||||
function isLambdaArn(arn: string): boolean {
|
||||
return arn.split(':')[2] === 'lambda'
|
||||
}
|
||||
|
||||
function permissionStatementId(busName: string, ruleName: string, targetId: string): string {
|
||||
return `floci-events-${busName}-${ruleName}-${targetId}`.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 100)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user