feat: Lightsail + svg animation

This commit is contained in:
Zakaria
2026-10-11 14:19:00 -04:00
parent 0036ef7e51
commit ca1e845cb6
40 changed files with 1887 additions and 34 deletions
@@ -0,0 +1,168 @@
import {
CloseInstancePublicPortsCommand,
CreateInstancesCommand,
DeleteInstanceCommand,
GetInstanceCommand,
GetInstancesCommand,
OpenInstancePublicPortsCommand,
type NetworkProtocol,
RebootInstanceCommand,
StartInstanceCommand,
StopInstanceCommand,
type Instance,
type LightsailClient,
} from '@aws-sdk/client-lightsail'
import {NotFoundError, ValidationError} from '../cloud-spi/errors'
import {awsLightsailSchema} from '../cloud-spi/lightsailSchema'
import {awsClients} from '../aws'
import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema, UpdateResourceInput} from '../cloud-spi/types'
export class AwsLightsailAdapter implements CloudServiceAdapter {
readonly cloud = 'aws' as const
readonly service = 'lightsail' as const
constructor(private readonly lightsail: LightsailClient = awsClients.lightsail) {}
schema(): ServiceSchema {
return awsLightsailSchema()
}
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
const response = await this.lightsail.send(new GetInstancesCommand({}))
const search = query.search?.trim().toLowerCase()
const resources = await Promise.all((response.instances ?? []).map((instance) => toResource(instance)))
return resources
.filter((instance) => !search || instance.name.toLowerCase().includes(search))
}
async get(id: string): Promise<CloudResource | null> {
try {
const response = await this.lightsail.send(new GetInstanceCommand({instanceName: id}))
return response.instance ? await toResource(response.instance) : null
} catch (error) {
if (isNotFound(error)) return null
throw error
}
}
async create(input: CreateResourceInput): Promise<CloudResource> {
const name = text(input.values.name)
const blueprintId = text(input.values.blueprintId)
const bundleId = text(input.values.bundleId)
const availabilityZone = text(input.values.availabilityZone)
if (!name || !blueprintId || !bundleId || !availabilityZone) {
throw new ValidationError('name, blueprintId, bundleId, and availabilityZone are required')
}
await this.lightsail.send(new CreateInstancesCommand({
instanceNames: [name], blueprintId, bundleId, availabilityZone,
}))
const instance = await this.get(name)
if (!instance) throw new ValidationError(`Lightsail did not return instance ${name} after creation`)
return instance
}
async delete(id: string): Promise<void> {
if (!await this.get(id)) throw new NotFoundError(`Lightsail instance ${id} does not exist`)
await this.lightsail.send(new DeleteInstanceCommand({instanceName: id}))
}
async update(id: string, input: UpdateResourceInput): Promise<CloudResource> {
const action = text(input.values.action)
const fromPort = port(input.values.fromPort)
const toPort = port(input.values.toPort)
const protocol = text(input.values.protocol)
if ((action !== 'openPort' && action !== 'closePort') || fromPort === null || toPort === null || fromPort > toPort || protocol !== 'tcp') {
throw new ValidationError('action (openPort or closePort), valid fromPort/toPort, and protocol tcp are required')
}
const portInfo = {fromPort, toPort, protocol: protocol as NetworkProtocol, cidrs: ['0.0.0.0/0']}
if (action === 'openPort') {
await this.lightsail.send(new OpenInstancePublicPortsCommand({instanceName: id, portInfo}))
} else {
await this.lightsail.send(new CloseInstancePublicPortsCommand({instanceName: id, portInfo}))
}
const instance = await this.get(id)
if (!instance) throw new NotFoundError(`Lightsail instance ${id} does not exist`)
return instance
}
async start(id: string): Promise<void> {
await this.lightsail.send(new StartInstanceCommand({instanceName: id}))
}
async stop(id: string): Promise<void> {
await this.lightsail.send(new StopInstanceCommand({instanceName: id}))
}
async reboot(id: string): Promise<void> {
await this.lightsail.send(new RebootInstanceCommand({instanceName: id}))
}
async health(): Promise<void> {
await this.lightsail.send(new GetInstancesCommand({}))
}
}
async function toResource(instance: Instance): Promise<CloudResource> {
const name = instance.name ?? ''
const hostPortMappings = await lightsailHostPortMappings(name)
return {
id: name,
name,
cloud: 'aws',
service: 'lightsail',
type: 'instance',
region: instance.location?.availabilityZone ?? null,
createdAt: instance.createdAt?.toISOString() ?? null,
status: instance.state?.name ?? null,
metadata: {
arn: instance.arn,
blueprintId: instance.blueprintId,
blueprintName: instance.blueprintName,
bundleId: instance.bundleId,
publicIpAddress: instance.publicIpAddress,
privateIpAddress: instance.privateIpAddress,
username: instance.username,
networking: instance.networking,
hardware: instance.hardware,
hostPortMappings,
},
}
}
type HostPortMapping = {instancePort: number; host: string; hostPort: number; protocol?: string}
async function lightsailHostPortMappings(instanceName: string): Promise<HostPortMapping[]> {
const endpoint = process.env.FLOCI_ENDPOINT
if (!endpoint || !instanceName) return []
try {
const response = await fetch(new URL(`/_floci/diagnostics/lightsail/instances/${encodeURIComponent(instanceName)}/host-ports`, endpoint), {
redirect: 'error', signal: AbortSignal.timeout(5_000),
})
if (!response.ok) return []
const body = await response.json() as {mappings?: unknown}
if (!Array.isArray(body.mappings)) return []
return body.mappings.flatMap((mapping): HostPortMapping[] => {
if (!mapping || typeof mapping !== 'object') return []
const value = mapping as {instancePort?: unknown; host?: unknown; hostPort?: unknown; protocol?: unknown}
return typeof value.instancePort === 'number' && typeof value.host === 'string' && typeof value.hostPort === 'number'
? [{instancePort: value.instancePort, host: value.host, hostPort: value.hostPort,
protocol: typeof value.protocol === 'string' ? value.protocol : undefined}]
: []
})
} catch {
return []
}
}
function text(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
function port(value: unknown): number | null {
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 65535 ? value : null
}
function isNotFound(error: unknown): boolean {
return typeof error === 'object' && error !== null
&& (error as {name?: string}).name === 'NotFoundException'
}