feat: MSK, MSK CONNECT and SES
This commit is contained in:
@@ -963,6 +963,64 @@ export function createCloudRoutes(injectedService?: CloudProxyService) {
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/:cloud/services/kinesis/resources/:id/records', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => {
|
||||
const input = await jsonBody<{partitionKey?: unknown; data?: unknown}>(c)
|
||||
return c.json(await svc(c).putKinesisRecord(cloud, c.req.param('id'), {
|
||||
partitionKey: typeof input.partitionKey === 'string' ? input.partitionKey : '',
|
||||
data: typeof input.data === 'string' ? input.data : '',
|
||||
}), 201)
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/:cloud/services/kinesis/resources/:id/records', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => c.json(await svc(c).listKinesisRecords(cloud, c.req.param('id'))))
|
||||
})
|
||||
|
||||
app.get('/:cloud/services/serverless/event-source-mappings', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => c.json(await svc(c).listLambdaEventSourceMappings(cloud, {
|
||||
functionName: c.req.query('functionName'), eventSourceArn: c.req.query('eventSourceArn'),
|
||||
})))
|
||||
})
|
||||
|
||||
app.post('/:cloud/services/serverless/event-source-mappings', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => {
|
||||
const input = await jsonBody<{functionName?: unknown; eventSourceArn?: unknown; batchSize?: unknown; startingPosition?: unknown}>(c)
|
||||
return c.json(await svc(c).createLambdaEventSourceMapping(cloud, {
|
||||
functionName: typeof input.functionName === 'string' ? input.functionName : '',
|
||||
eventSourceArn: typeof input.eventSourceArn === 'string' ? input.eventSourceArn : '',
|
||||
batchSize: typeof input.batchSize === 'number' ? input.batchSize : undefined,
|
||||
startingPosition: input.startingPosition === 'TRIM_HORIZON' || input.startingPosition === 'LATEST' ? input.startingPosition : undefined,
|
||||
}), 201)
|
||||
})
|
||||
})
|
||||
|
||||
app.patch('/:cloud/services/serverless/event-source-mappings/:mappingId', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => {
|
||||
const input = await jsonBody<{batchSize?: unknown; enabled?: unknown}>(c)
|
||||
return c.json(await svc(c).updateLambdaEventSourceMapping(cloud, c.req.param('mappingId'), {
|
||||
batchSize: typeof input.batchSize === 'number' ? input.batchSize : undefined,
|
||||
enabled: typeof input.enabled === 'boolean' ? input.enabled : undefined,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
app.delete('/:cloud/services/serverless/event-source-mappings/:mappingId', async (c) => {
|
||||
const cloud = c.req.param('cloud') as CloudProvider
|
||||
if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
return withRuntime(c, async () => { await svc(c).deleteLambdaEventSourceMapping(cloud, c.req.param('mappingId')); return c.json({ok: true}) })
|
||||
})
|
||||
|
||||
app.get('/:cloud/services/:service/resources/:id/collections', async (c) => {
|
||||
const target = childTarget(c)
|
||||
if (!target) return c.json({error: 'Unknown cloud or service'}, 404)
|
||||
|
||||
@@ -75,6 +75,7 @@ const fakeService = {
|
||||
createImage: async (_input: CreateAmiInput): Promise<{imageId: string}> => ({imageId: 'ami-0a1b2c3d4e5f6a7b'}),
|
||||
deregisterImage: async () => {},
|
||||
getConsoleOutput: async (instanceId: string): Promise<Ec2ConsoleOutput> => ({instanceId, output: 'boot log'}),
|
||||
updateInstanceSecurityGroups: async () => {},
|
||||
listAmis: async (): Promise<Ec2Image[]> => [],
|
||||
listKeyPairs: async (): Promise<Ec2KeyPair[]> => [],
|
||||
createKeyPair: async (name: string, _keyType: Ec2KeyPairType = 'rsa'): Promise<Ec2KeyPairMaterial> => ({keyPairId: 'key-0a1b2c3d4e5f6a7b', keyName: name, keyMaterial: '---PEM---'}),
|
||||
@@ -201,6 +202,22 @@ describe('POST /instances/:id/terminate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /instances/:id/security-groups', () => {
|
||||
test('replaces security groups', async () => {
|
||||
let receivedGroupIds: string[] | undefined
|
||||
const router = createEc2Router({...fakeService, updateInstanceSecurityGroups: async (_instanceId, groupIds) => {
|
||||
receivedGroupIds = groupIds
|
||||
}})
|
||||
const res = await router.request('/instances/i-test1/security-groups', {
|
||||
method: 'PUT',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({groupIds: ['sg-123']}),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(receivedGroupIds).toEqual(['sg-123'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /amis/:imageId/deregister', () => {
|
||||
test('deregisters AMI and returns ok', async () => {
|
||||
const res = await app.request('/amis/ami-0a1b2c3d4e5f6a7b/deregister', {method: 'POST'})
|
||||
|
||||
@@ -15,6 +15,31 @@ import {ec2Service} from '../services/ec2'
|
||||
type Ec2Service = typeof ec2Service
|
||||
type CreateKeyPairInput = {name: string; keyType?: Ec2KeyPairType}
|
||||
|
||||
type Ec2DiagnosticResponse = {ec2?: {instances?: Array<{
|
||||
instanceId?: string
|
||||
sshHost?: string
|
||||
sshPort?: number
|
||||
containerId?: string
|
||||
containerStatus?: 'running' | 'stopped' | 'unavailable'
|
||||
}>}}
|
||||
|
||||
async function connectionDetails(instanceId: string) {
|
||||
const endpoint = process.env.FLOCI_ENDPOINT
|
||||
if (!endpoint) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/_floci/diagnose`)
|
||||
if (!response.ok) {
|
||||
return undefined
|
||||
}
|
||||
return (await response.json() as Ec2DiagnosticResponse).ec2?.instances
|
||||
?.find((instance) => instance.instanceId === instanceId)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function isEc2KeyPairType(value: unknown): value is Ec2KeyPairType {
|
||||
return value === 'rsa' || value === 'ed25519'
|
||||
}
|
||||
@@ -33,7 +58,9 @@ export function createEc2Router(svc: Ec2Service = ec2Service) {
|
||||
})
|
||||
|
||||
app.get('/instances/:instanceId', async (c) => {
|
||||
return c.json(await svc.describeInstance(c.req.param('instanceId')))
|
||||
const instanceId = c.req.param('instanceId')
|
||||
const [instance, connection] = await Promise.all([svc.describeInstance(instanceId), connectionDetails(instanceId)])
|
||||
return c.json({...instance, connection})
|
||||
})
|
||||
|
||||
app.post('/instances', async (c) => {
|
||||
@@ -91,6 +118,19 @@ export function createEc2Router(svc: Ec2Service = ec2Service) {
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/instances/:instanceId/security-groups', async (c) => {
|
||||
try {
|
||||
const {groupIds} = await c.req.json<{groupIds: string[]}>()
|
||||
if (!Array.isArray(groupIds) || groupIds.length === 0 || groupIds.some((groupId) => !groupId)) {
|
||||
return c.json({error: 'At least one security group is required'}, 400)
|
||||
}
|
||||
await svc.updateInstanceSecurityGroups(c.req.param('instanceId'), groupIds)
|
||||
return c.json({ok: true})
|
||||
} catch (error) {
|
||||
return handleError(c, error)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/instances/:instanceId/image', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json<Omit<CreateAmiInput, 'instanceId'>>()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createMskRouter } from "./msk";
|
||||
import { type mskService } from "../services/msk";
|
||||
|
||||
function createService() {
|
||||
return {
|
||||
listClusters: async () => [{ ClusterName: "orders" }],
|
||||
createCluster: async () => ({ ClusterArn: "arn:cluster:orders" }),
|
||||
listTopics: async () => [{ name: "orders", partitionCount: 1 }],
|
||||
createTopic: async () => {},
|
||||
deleteTopic: async () => {},
|
||||
produceMessage: async () => [{ topicName: "orders", partition: 0, baseOffset: "0" }],
|
||||
peekMessages: async () => [{ value: "hello" }],
|
||||
consumerGroupLag: async () => [{ topic: "orders", totalLag: 0, partitions: [] }],
|
||||
} as unknown as typeof mskService;
|
||||
}
|
||||
|
||||
describe("msk routes", () => {
|
||||
test("serves clusters", async () => {
|
||||
const app = createMskRouter(createService());
|
||||
|
||||
const clusters = await app.request("/clusters");
|
||||
expect(clusters.status).toBe(200);
|
||||
expect(await clusters.json()).toEqual([{ ClusterName: "orders" }]);
|
||||
|
||||
});
|
||||
|
||||
test("serves topic, message, and consumer group operations through the API", async () => {
|
||||
const app = createMskRouter(createService());
|
||||
|
||||
const topic = await app.request("/clusters/cluster-arn/topics");
|
||||
expect(await topic.json()).toEqual([{ name: "orders", partitionCount: 1 }]);
|
||||
|
||||
const create = await app.request("/clusters/cluster-arn/topics", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ topic: "events" }),
|
||||
});
|
||||
expect(create.status).toBe(201);
|
||||
|
||||
const produced = await app.request("/clusters/cluster-arn/topics/orders/messages", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ value: "hello" }),
|
||||
});
|
||||
expect(produced.status).toBe(201);
|
||||
|
||||
const messages = await app.request("/clusters/cluster-arn/topics/orders/messages?limit=1");
|
||||
expect(await messages.json()).toEqual([{ value: "hello" }]);
|
||||
|
||||
const lag = await app.request("/clusters/cluster-arn/consumer-groups/orders-workers/lag");
|
||||
expect(await lag.json()).toEqual([{ topic: "orders", totalLag: 0, partitions: [] }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
mskService,
|
||||
type CreateMskClusterInput,
|
||||
type CreateTopicInput,
|
||||
type ProduceMessageInput,
|
||||
} from "../services/msk";
|
||||
|
||||
type MskService = typeof mskService;
|
||||
|
||||
export function createMskRouter(service: MskService = mskService) {
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/clusters", async (c) => c.json(await service.listClusters()));
|
||||
app.post("/clusters", async (c) => c.json(await service.createCluster(await c.req.json<CreateMskClusterInput>()), 201));
|
||||
app.delete("/clusters/:clusterArn", async (c) => {
|
||||
await service.deleteCluster(c.req.param("clusterArn"));
|
||||
return c.json({ state: "DELETING" });
|
||||
});
|
||||
app.get("/clusters/:clusterArn/bootstrap-brokers", async (c) => c.json({bootstrapBrokers: await service.getBootstrapBrokers(c.req.param("clusterArn"))}));
|
||||
app.get("/clusters/:clusterArn/topics", async (c) => c.json(await service.listTopics(c.req.param("clusterArn"))));
|
||||
app.post("/clusters/:clusterArn/topics", async (c) => c.json(await service.createTopic(c.req.param("clusterArn"), await c.req.json<CreateTopicInput>()), 201));
|
||||
app.delete("/clusters/:clusterArn/topics/:topic", async (c) => c.json(await service.deleteTopic(c.req.param("clusterArn"), c.req.param("topic"))));
|
||||
app.post("/clusters/:clusterArn/topics/:topic/messages", async (c) => c.json(await service.produceMessage(c.req.param("clusterArn"), c.req.param("topic"), await c.req.json<ProduceMessageInput>()), 201));
|
||||
app.get("/clusters/:clusterArn/topics/:topic/messages", async (c) => c.json(await service.peekMessages(c.req.param("clusterArn"), c.req.param("topic"), Number(c.req.query("limit") ?? 50))));
|
||||
app.get("/clusters/:clusterArn/consumer-groups/:groupId/lag", async (c) => c.json(await service.consumerGroupLag(c.req.param("clusterArn"), c.req.param("groupId"), c.req.query("topic"))));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export default createMskRouter();
|
||||
@@ -0,0 +1,20 @@
|
||||
import {describe, expect, test} from "bun:test";
|
||||
import {createMskConnectRouter} from "./mskConnect";
|
||||
import {type mskConnectService} from "../services/mskConnect";
|
||||
|
||||
describe("MSK Connect routes", () => {
|
||||
test("passes custom plugin and connector operations through the API", async () => {
|
||||
const app = createMskConnectRouter({
|
||||
listCustomPlugins: async () => [{name: "s3-sink"}], createCustomPlugin: async () => ({name: "s3-sink"}), listConnectors: async () => [{connectorName: "archive"}], createConnector: async () => ({connectorArn: "arn:archive"}), describeConnector: async () => ({connectorArn: "arn:archive", connectorState: "RUNNING"}), deleteConnector: async () => ({}), updateConnector: async () => ({connectorState: "UPDATING"}),
|
||||
} as unknown as typeof mskConnectService);
|
||||
|
||||
expect((await app.request("/custom-plugins")).status).toBe(200);
|
||||
const register = await app.request("/custom-plugins", {method: "POST", headers: {"content-type": "application/json"}, body: JSON.stringify({name: "s3-sink", contentType: "ZIP"})});
|
||||
expect(register.status).toBe(201);
|
||||
const create = await app.request("/connectors", {method: "POST", headers: {"content-type": "application/json"}, body: JSON.stringify({connectorName: "archive"})});
|
||||
expect(create.status).toBe(201);
|
||||
expect(await (await app.request("/connectors/arn%3Aarchive")).json()).toMatchObject({connectorState: "RUNNING"});
|
||||
expect((await app.request("/connectors/arn%3Aarchive", {method: "PATCH", headers: {"content-type": "application/json"}, body: JSON.stringify({currentVersion: "1"})})).status).toBe(200);
|
||||
expect((await app.request("/connectors/arn%3Aarchive", {method: "DELETE"})).status).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Hono } from "hono";
|
||||
import { mskConnectService } from "../services/mskConnect";
|
||||
|
||||
type MskConnectService = typeof mskConnectService;
|
||||
|
||||
export function createMskConnectRouter(service: MskConnectService = mskConnectService) {
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/custom-plugins", async (c) => c.json(await service.listCustomPlugins()));
|
||||
app.post("/custom-plugins", async (c) => c.json(await service.createCustomPlugin(await c.req.json()), 201));
|
||||
app.delete("/custom-plugins/:customPluginArn", async (c) => c.json(await service.deleteCustomPlugin(c.req.param("customPluginArn"))));
|
||||
app.get("/connectors", async (c) => c.json(await service.listConnectors()));
|
||||
app.post("/connectors", async (c) => c.json(await service.createConnector(await c.req.json()), 201));
|
||||
app.get("/connectors/:connectorArn", async (c) => c.json(await service.describeConnector(c.req.param("connectorArn"))));
|
||||
app.delete("/connectors/:connectorArn", async (c) => c.json(await service.deleteConnector(c.req.param("connectorArn"))));
|
||||
app.patch("/connectors/:connectorArn", async (c) => c.json(await service.updateConnector({
|
||||
...(await c.req.json()),
|
||||
connectorArn: c.req.param("connectorArn"),
|
||||
})));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export default createMskConnectRouter();
|
||||
@@ -0,0 +1,55 @@
|
||||
import {Hono} from "hono";
|
||||
import {
|
||||
sesService,
|
||||
type RawEmailInput,
|
||||
} from "../services/ses";
|
||||
import type {
|
||||
CreateConfigurationSetCommandInput,
|
||||
CreateConfigurationSetEventDestinationCommandInput,
|
||||
CreateEmailIdentityCommandInput,
|
||||
CreateEmailTemplateCommandInput,
|
||||
PutSuppressedDestinationCommandInput,
|
||||
SendEmailCommandInput,
|
||||
TestRenderEmailTemplateCommandInput,
|
||||
UpdateEmailTemplateCommandInput,
|
||||
} from "@aws-sdk/client-sesv2";
|
||||
|
||||
type SesService = typeof sesService;
|
||||
|
||||
export function createSesRouter(service: SesService = sesService) {
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/identities", async (c) => c.json(await service.listIdentities()));
|
||||
app.post("/identities", async (c) => c.json(await service.createIdentity(await c.req.json<CreateEmailIdentityCommandInput>()), 201));
|
||||
app.delete("/identities/:identity", async (c) => c.json(await service.deleteIdentity(c.req.param("identity"))));
|
||||
|
||||
app.post("/emails/simple", async (c) => c.json(await service.sendSimple(await c.req.json<SendEmailCommandInput>()), 201));
|
||||
app.post("/emails/raw", async (c) => c.json(await service.sendRaw(await c.req.json<RawEmailInput>()), 201));
|
||||
app.post("/emails/template", async (c) => c.json(await service.sendTemplate(await c.req.json<SendEmailCommandInput>()), 201));
|
||||
|
||||
app.get("/suppressions", async (c) => c.json(await service.listSuppressedDestinations()));
|
||||
app.post("/suppressions", async (c) => c.json(await service.createSuppression(await c.req.json<PutSuppressedDestinationCommandInput>()), 201));
|
||||
app.delete("/suppressions/:emailAddress", async (c) => c.json(await service.deleteSuppression(c.req.param("emailAddress"))));
|
||||
|
||||
app.get("/templates", async (c) => c.json(await service.listTemplates()));
|
||||
app.get("/templates/:templateName", async (c) => c.json(await service.getTemplate(c.req.param("templateName"))));
|
||||
app.post("/templates", async (c) => c.json(await service.createTemplate(await c.req.json<CreateEmailTemplateCommandInput>()), 201));
|
||||
app.put("/templates/:templateName", async (c) => c.json(await service.updateTemplate(c.req.param("templateName"), await c.req.json<Omit<UpdateEmailTemplateCommandInput, "TemplateName">>())));
|
||||
app.delete("/templates/:templateName", async (c) => c.json(await service.deleteTemplate(c.req.param("templateName"))));
|
||||
app.post("/templates/test-render", async (c) => c.json(await service.testRenderTemplate(await c.req.json<TestRenderEmailTemplateCommandInput>())));
|
||||
|
||||
app.get("/configuration-sets", async (c) => c.json(await service.listConfigurationSets()));
|
||||
app.post("/configuration-sets", async (c) => c.json(await service.createConfigurationSet(await c.req.json<CreateConfigurationSetCommandInput>()), 201));
|
||||
app.delete("/configuration-sets/:configurationSetName", async (c) => c.json(await service.deleteConfigurationSet(c.req.param("configurationSetName"))));
|
||||
app.get("/configuration-sets/:configurationSetName/event-destinations", async (c) => c.json(await service.listEventDestinations(c.req.param("configurationSetName"))));
|
||||
app.post("/configuration-sets/:configurationSetName/event-destinations", async (c) => c.json(await service.createEventDestination(c.req.param("configurationSetName"), await c.req.json<Omit<CreateConfigurationSetEventDestinationCommandInput, "ConfigurationSetName">>()), 201));
|
||||
app.delete("/configuration-sets/:configurationSetName/event-destinations/:eventDestinationName", async (c) => c.json(await service.deleteEventDestination(c.req.param("configurationSetName"), c.req.param("eventDestinationName"))));
|
||||
|
||||
app.get("/inventory/sns-topics", async (c) => c.json(await service.listSnsTopics()));
|
||||
app.get("/inventory/event-buses", async (c) => c.json(await service.listEventBuses()));
|
||||
app.get("/inventory/cloudwatch-metrics", async (c) => c.json(await service.listSesMetrics()));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export default createSesRouter();
|
||||
Reference in New Issue
Block a user