import {Hono} from 'hono' import type {Context} from 'hono' import type { CreateAmiInput, CreateNatGatewayInput, CreateRouteInput, Ec2KeyPairType, Ec2Tag, IpPermissionInput, RunInstanceInput, VpcWizardInput, } from '../services/ec2' import {ec2Service} from '../services/ec2' import {SshCheckError, sshCheckService} from '../services/sshCheck' type Ec2Service = typeof ec2Service type SshCheckService = typeof sshCheckService type CreateKeyPairInput = {name: string; keyType?: Ec2KeyPairType} type Ec2DiagnosticResponse = {ec2?: {instances?: Array<{ instanceId?: string sshHost?: string sshPort?: number containerId?: string containerStatus?: 'running' | 'stopped' | 'unavailable' state?: string launchStatus?: string launchEvents?: Array<{stage?: string; status?: string; timestamp?: string; message?: string}> selectedImageType?: string selectedImageReference?: string sshUser?: string sshPreinstalled?: boolean initType?: string sshServiceName?: string sshStatus?: string sshStatusMessage?: string keyName?: string keyInjectionStatus?: string publishedPorts?: Record failure?: {code?: string; message?: string} | string }>}} 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' } function handleError(c: Context, error: unknown) { const msg = error instanceof Error ? error.message : 'Unknown error' return c.json({error: msg}, 400) } export function createEc2Router(svc: Ec2Service = ec2Service, sshChecker: SshCheckService = sshCheckService) { const app = new Hono() // Instances app.get('/instances', async (c) => { return c.json(await svc.listInstances()) }) app.get('/instances/:instanceId', async (c) => { 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) => { try { const input = await c.req.json() return c.json(await svc.runInstance(input)) } catch (error) { return handleError(c, error) } }) app.post('/instances/:instanceId/start', async (c) => { try { await svc.startInstance(c.req.param('instanceId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/instances/:instanceId/stop', async (c) => { try { await svc.stopInstance(c.req.param('instanceId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/instances/:instanceId/reboot', async (c) => { try { await svc.rebootInstance(c.req.param('instanceId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/instances/:instanceId/terminate', async (c) => { try { await svc.terminateInstance(c.req.param('instanceId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/instances/:instanceId/ssh/check', async (c) => { try { return c.json(await sshChecker.check(c.req.param('instanceId'))) } catch (error) { if (error instanceof SshCheckError) { return c.json({ok: false, code: error.code, error: error.message}, error.status) } return c.json({ok: false, code: 'DIAGNOSTICS_UNAVAILABLE', error: 'SSH check failed'}, 502) } }) app.put('/instances/:instanceId/tags', async (c) => { try { const {toAdd, toRemove} = await c.req.json<{toAdd: Ec2Tag[]; toRemove: string[]}>() if (!Array.isArray(toAdd) || !Array.isArray(toRemove)) { return c.json({error: 'toAdd and toRemove must be arrays'}, 400) } await svc.updateInstanceTags(c.req.param('instanceId'), toAdd, toRemove) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) 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>() const result = await svc.createImage({...body, instanceId: c.req.param('instanceId')}) return c.json(result) } catch (error) { return handleError(c, error) } }) app.get('/instances/:instanceId/console', async (c) => { try { return c.json(await svc.getConsoleOutput(c.req.param('instanceId'))) } catch (error) { return handleError(c, error) } }) // AMIs app.get('/amis', async (c) => { return c.json(await svc.listAmis()) }) app.post('/amis/:imageId/deregister', async (c) => { try { await svc.deregisterImage(c.req.param('imageId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Key pairs app.get('/key-pairs', async (c) => { return c.json(await svc.listKeyPairs()) }) app.post('/key-pairs', async (c) => { try { const {name, keyType = 'rsa'} = await c.req.json() if (!isEc2KeyPairType(keyType)) { return c.json({error: 'keyType must be rsa or ed25519'}, 400) } return c.json(await svc.createKeyPair(name, keyType)) } catch (error) { return handleError(c, error) } }) app.delete('/key-pairs/:name', async (c) => { try { await svc.deleteKeyPair(c.req.param('name')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Security groups app.get('/security-groups', async (c) => { const vpcId = c.req.query('vpcId') return c.json(await svc.listSecurityGroups(vpcId)) }) app.post('/security-groups', async (c) => { try { const {name, description, vpcId} = await c.req.json<{name: string; description: string; vpcId: string}>() return c.json(await svc.createSecurityGroup(name, description, vpcId)) } catch (error) { return handleError(c, error) } }) app.delete('/security-groups/:groupId', async (c) => { try { await svc.deleteSecurityGroup(c.req.param('groupId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/security-groups/:groupId/ingress', async (c) => { try { const permission = await c.req.json() await svc.authorizeSecurityGroupIngress(c.req.param('groupId'), permission) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.delete('/security-groups/:groupId/ingress', async (c) => { try { const permission = await c.req.json() await svc.revokeSecurityGroupIngress(c.req.param('groupId'), permission) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/security-groups/:groupId/egress', async (c) => { try { const permission = await c.req.json() await svc.authorizeSecurityGroupEgress(c.req.param('groupId'), permission) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.delete('/security-groups/:groupId/egress', async (c) => { try { const permission = await c.req.json() await svc.revokeSecurityGroupEgress(c.req.param('groupId'), permission) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Meta app.get('/availability-zones', async (c) => { return c.json(await svc.listAvailabilityZones()) }) app.get('/instance-types', async (c) => { return c.json(await svc.listInstanceTypes()) }) // VPCs app.get('/vpcs', async (c) => { return c.json(await svc.listVpcs()) }) app.post('/vpcs', async (c) => { try { const {cidrBlock} = await c.req.json<{cidrBlock: string}>() return c.json(await svc.createVpc(cidrBlock)) } catch (error) { return handleError(c, error) } }) app.delete('/vpcs/:vpcId', async (c) => { try { await svc.deleteVpc(c.req.param('vpcId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.get('/vpcs/:vpcId/attributes', async (c) => { try { return c.json(await svc.getVpcAttributes(c.req.param('vpcId'))) } catch (error) { return handleError(c, error) } }) app.put('/vpcs/:vpcId/attributes', async (c) => { try { const {attribute, value} = await c.req.json<{attribute: 'enableDnsHostnames' | 'enableDnsSupport'; value: boolean}>() await svc.modifyVpcAttribute(c.req.param('vpcId'), attribute, value) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Subnets app.get('/subnets', async (c) => { const vpcId = c.req.query('vpcId') return c.json(await svc.listSubnets(vpcId)) }) app.post('/subnets', async (c) => { try { const {vpcId, cidrBlock, availabilityZone} = await c.req.json<{ vpcId: string cidrBlock: string availabilityZone?: string }>() return c.json(await svc.createSubnet(vpcId, cidrBlock, availabilityZone)) } catch (error) { return handleError(c, error) } }) app.delete('/subnets/:subnetId', async (c) => { try { await svc.deleteSubnet(c.req.param('subnetId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.put('/subnets/:subnetId/attributes', async (c) => { try { const {mapPublicIpOnLaunch} = await c.req.json<{mapPublicIpOnLaunch: boolean}>() await svc.modifySubnetMapPublicIp(c.req.param('subnetId'), mapPublicIpOnLaunch) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Internet Gateways app.get('/internet-gateways', async (c) => { return c.json(await svc.listInternetGateways()) }) app.post('/internet-gateways', async (c) => { try { const body = await c.req.json<{name?: string}>().catch(() => ({} as {name?: string})) return c.json(await svc.createInternetGateway(body.name)) } catch (error) { return handleError(c, error) } }) app.post('/internet-gateways/:igwId/attach', async (c) => { try { const {vpcId} = await c.req.json<{vpcId: string}>() await svc.attachInternetGateway(c.req.param('igwId'), vpcId) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/internet-gateways/:igwId/detach', async (c) => { try { const {vpcId} = await c.req.json<{vpcId: string}>() await svc.detachInternetGateway(c.req.param('igwId'), vpcId) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.delete('/internet-gateways/:igwId', async (c) => { try { await svc.deleteInternetGateway(c.req.param('igwId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // NAT Gateways app.get('/nat-gateways', async (c) => { const vpcId = c.req.query('vpcId') return c.json(await svc.listNatGateways(vpcId)) }) app.post('/nat-gateways', async (c) => { try { const input = await c.req.json() return c.json(await svc.createNatGateway(input)) } catch (error) { return handleError(c, error) } }) app.delete('/nat-gateways/:natId', async (c) => { try { await svc.deleteNatGateway(c.req.param('natId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Route Tables app.get('/route-tables', async (c) => { const vpcId = c.req.query('vpcId') return c.json(await svc.listRouteTables(vpcId)) }) app.post('/route-tables', async (c) => { try { const {vpcId, name} = await c.req.json<{vpcId: string; name?: string}>() return c.json(await svc.createRouteTable(vpcId, name)) } catch (error) { return handleError(c, error) } }) app.delete('/route-tables/:rtbId', async (c) => { try { await svc.deleteRouteTable(c.req.param('rtbId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/route-tables/:rtbId/routes', async (c) => { try { const input = await c.req.json() await svc.createRoute(c.req.param('rtbId'), input) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.delete('/route-tables/:rtbId/routes', async (c) => { try { const cidr = c.req.query('cidr') if (!cidr) return handleError(c, new Error('cidr query parameter is required')) await svc.deleteRoute(c.req.param('rtbId'), cidr) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/route-tables/:rtbId/associations', async (c) => { try { const {subnetId} = await c.req.json<{subnetId: string}>() const associationId = await svc.associateRouteTable(c.req.param('rtbId'), subnetId) return c.json({associationId}) } catch (error) { return handleError(c, error) } }) app.delete('/route-table-associations/:associationId', async (c) => { try { await svc.disassociateRouteTable(c.req.param('associationId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // Elastic IPs app.get('/elastic-ips', async (c) => { return c.json(await svc.listElasticIps()) }) app.post('/elastic-ips', async (c) => { try { const body = await c.req.json<{name?: string}>().catch(() => ({} as {name?: string})) return c.json(await svc.allocateElasticIp(body.name)) } catch (error) { return handleError(c, error) } }) app.post('/elastic-ips/:allocationId/release', async (c) => { try { await svc.releaseElasticIp(c.req.param('allocationId')) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) app.post('/elastic-ips/:allocationId/associate', async (c) => { try { const {instanceId} = await c.req.json<{instanceId: string}>() return c.json(await svc.associateElasticIp(c.req.param('allocationId'), instanceId)) } catch (error) { return handleError(c, error) } }) app.post('/elastic-ips/:allocationId/disassociate', async (c) => { try { const {associationId} = await c.req.json<{associationId: string}>() await svc.disassociateElasticIp(associationId) return c.json({ok: true}) } catch (error) { return handleError(c, error) } }) // VPC Wizard — creates VPC + IGW + subnets + route tables + optional NAT GW in one call app.post('/vpc-wizard', async (c) => { try { const input = await c.req.json() const result = await svc.createVpcWizard(input) return c.json(result) } catch (error) { return handleError(c, error) } }) return app } export default createEc2Router()