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,42 @@
import {afterEach, describe, expect, mock, test} from 'bun:test'
import {deployLightsailNodeApplication, deployLightsailStaticFiles} from './lightsailDeployment'
const originalEndpoint = process.env.FLOCI_ENDPOINT
const originalFetch = globalThis.fetch
afterEach(() => {
process.env.FLOCI_ENDPOINT = originalEndpoint
globalThis.fetch = originalFetch
mock.restore()
})
describe('deployLightsailNodeApplication', () => {
test('forwards package uploads and preserves core deployment diagnostics', async () => {
process.env.FLOCI_ENDPOINT = 'http://core.test:4566'
const fetch = mock(() => Promise.resolve(new Response(JSON.stringify({status: 'deployed', files: 2, port: 3000, releaseId: 'release'}), {headers: {'content-type': 'application/json'}})))
globalThis.fetch = fetch as unknown as typeof globalThis.fetch
const form = new FormData()
form.append('files', new File(['{}'], 'package.json'))
form.append('port', '3000')
await expect(deployLightsailNodeApplication('node app', form)).resolves.toEqual({status: 'deployed', files: 2, port: 3000, releaseId: 'release'})
expect(fetch).toHaveBeenCalledWith('http://core.test:4566/_floci/diagnostics/lightsail/instances/node%20app/node-application', expect.objectContaining({
method: 'POST', body: form, redirect: 'error',
}))
})
})
describe('deployLightsailStaticFiles', () => {
test('forwards multipart files only to the diagnostics deployment endpoint', async () => {
process.env.FLOCI_ENDPOINT = 'http://core.test:4566'
const fetch = mock(() => Promise.resolve(new Response(JSON.stringify({status: 'deployed', files: 1}), {headers: {'content-type': 'application/json'}})))
globalThis.fetch = fetch as unknown as typeof globalThis.fetch
const form = new FormData()
form.append('files', new File(['<h1>ok</h1>'], 'site/index.html'))
await expect(deployLightsailStaticFiles('web site', form)).resolves.toEqual({status: 'deployed', files: 1})
expect(fetch).toHaveBeenCalledWith('http://core.test:4566/_floci/diagnostics/lightsail/instances/web%20site/static-files', expect.objectContaining({
method: 'POST', body: form, redirect: 'error',
}))
})
})
@@ -0,0 +1,44 @@
import {httpStatusToCloudError, RuntimeUnavailableError} from '../cloud-spi/errors'
export type LightsailStaticDeployment = {status: 'deployed'; files: number}
export type LightsailNodeDeployment = {status: 'deployed'; files: number; port: number; releaseId: string}
export async function deployLightsailStaticFiles(instanceName: string, form: FormData): Promise<LightsailStaticDeployment> {
const endpoint = process.env.FLOCI_ENDPOINT
if (!endpoint) throw new RuntimeUnavailableError('Floci diagnostics are unavailable')
let response: Response
try {
response = await fetch(new URL(`/_floci/diagnostics/lightsail/instances/${encodeURIComponent(instanceName)}/static-files`, endpoint).toString(), {
method: 'POST', body: form, redirect: 'error', signal: AbortSignal.timeout(30_000),
})
} catch (error) {
throw new RuntimeUnavailableError('Floci diagnostics are unavailable', {cause: error})
}
const body = await response.json().catch(() => ({})) as {message?: unknown; files?: unknown; status?: unknown}
if (!response.ok) throw httpStatusToCloudError(response.status, typeof body.message === 'string' ? body.message : 'Static file deployment failed')
if (body.status !== 'deployed' || typeof body.files !== 'number') {
throw new RuntimeUnavailableError('Floci diagnostics returned an invalid deployment response')
}
return {status: 'deployed', files: body.files}
}
export async function deployLightsailNodeApplication(instanceName: string, form: FormData): Promise<LightsailNodeDeployment> {
const endpoint = process.env.FLOCI_ENDPOINT
if (!endpoint) throw new RuntimeUnavailableError('Floci diagnostics are unavailable')
let response: Response
try {
response = await fetch(new URL(`/_floci/diagnostics/lightsail/instances/${encodeURIComponent(instanceName)}/node-application`, endpoint).toString(), {
method: 'POST', body: form, redirect: 'error', signal: AbortSignal.timeout(360_000),
})
} catch (error) {
throw new RuntimeUnavailableError('Floci diagnostics are unavailable', {cause: error})
}
const body = await response.json().catch(() => ({})) as {message?: unknown; files?: unknown; status?: unknown; port?: unknown; releaseId?: unknown}
if (!response.ok) throw httpStatusToCloudError(response.status, typeof body.message === 'string' ? body.message : 'Node.js deployment failed')
if (body.status !== 'deployed' || typeof body.files !== 'number' || typeof body.port !== 'number' || typeof body.releaseId !== 'string') {
throw new RuntimeUnavailableError('Floci diagnostics returned an invalid Node.js deployment response')
}
return {status: 'deployed', files: body.files, port: body.port, releaseId: body.releaseId}
}
+157
View File
@@ -0,0 +1,157 @@
import {afterEach, describe, expect, test} from 'bun:test'
import {createServer, type Server} from 'node:net'
import {
createSshCheckService,
readSshBanner,
SshCheckError,
type SshTarget,
} from './sshCheck'
const servers: Server[] = []
afterEach(async () => {
await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve()))))
})
async function listen(handler: (socket: import('node:net').Socket) => void): Promise<SshTarget> {
const server = createServer(handler)
servers.push(server)
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Test server has no TCP address')
return {host: '127.0.0.1', port: address.port}
}
describe('readSshBanner', () => {
test('reads a real SSH identification banner', async () => {
const target = await listen((socket) => socket.end('SSH-2.0-OpenSSH_9.7\r\n'))
expect(await readSshBanner(target, 500)).toBe('SSH-2.0-OpenSSH_9.7')
})
test('assembles a fragmented banner', async () => {
const target = await listen((socket) => {
socket.write('SSH-2.0-Open')
setTimeout(() => socket.end('SSH_9.7\r\n'), 10)
})
expect(await readSshBanner(target, 500)).toBe('SSH-2.0-OpenSSH_9.7')
})
test('rejects a connection that closes without an SSH banner', async () => {
const target = await listen((socket) => socket.end('HTTP/1.1 200 OK\r\n'))
await expect(readSshBanner(target, 500)).rejects.toMatchObject({code: 'SSH_BANNER_MISSING'})
})
test('rejects an SSH identification line longer than the protocol limit', async () => {
const target = await listen((socket) => socket.end(`SSH-2.0-${'x'.repeat(246)}\r\n`))
await expect(readSshBanner(target, 500)).rejects.toMatchObject({code: 'SSH_BANNER_MISSING'})
})
test('maps a silent socket to a timeout', async () => {
const target = await listen(() => {})
await expect(readSshBanner(target, 20)).rejects.toMatchObject({code: 'CONNECTION_TIMEOUT'})
})
test('maps a refused socket connection', async () => {
const temporaryServer = createServer()
await new Promise<void>((resolve) => temporaryServer.listen(0, '127.0.0.1', resolve))
const address = temporaryServer.address()
if (!address || typeof address === 'string') throw new Error('Test server has no TCP address')
const port = address.port
await new Promise<void>((resolve) => temporaryServer.close(() => resolve()))
await expect(readSshBanner({host: '127.0.0.1', port}, 500)).rejects.toMatchObject({code: 'CONNECTION_REFUSED'})
})
})
describe('sshCheckService', () => {
test('derives the exact instance target and overrides a loopback host', async () => {
let receivedTarget: SshTarget | undefined
const service = createSshCheckService({
fetchDiagnostics: async () => ({ec2: {instances: [
{instanceId: 'i-other', sshHost: '192.0.2.10', sshPort: 2200, containerStatus: 'running'},
{instanceId: 'i-wanted', sshHost: '127.0.0.1', sshPort: 2222, containerStatus: 'running'},
]}}),
loopbackHost: 'ssh-host.test',
connect: async (target) => {
receivedTarget = target
return 'SSH-2.0-test'
},
now: () => new Date('2026-10-08T12:00:00.000Z'),
})
const result = await service.check('i-wanted')
expect(receivedTarget).toEqual({host: 'ssh-host.test', port: 2222})
expect(result).toEqual({
ok: true,
banner: 'SSH-2.0-test',
checkedFrom: 'UI backend',
checkedAt: '2026-10-08T12:00:00.000Z',
target: {host: 'ssh-host.test', port: 2222},
})
})
test('does not allow diagnostics to redirect the backend probe', async () => {
let receivedTarget: SshTarget | undefined
const service = createSshCheckService({
fetchDiagnostics: async () => ({ec2: {instances: [
{instanceId: 'i-wanted', sshHost: '192.0.2.20', sshPort: 2222, containerStatus: 'running'},
]}}),
loopbackHost: 'ssh-host.test',
connect: async (target) => {
receivedTarget = target
return 'SSH-2.0-test'
},
})
await service.check('i-wanted')
expect(receivedTarget).toEqual({host: 'ssh-host.test', port: 2222})
})
test('reports malformed diagnostics before attempting a connection', async () => {
const service = createSshCheckService({
fetchDiagnostics: async () => ({ec2: {instances: {} as never}}),
connect: async () => { throw new Error('must not connect') },
})
await expect(service.check('i-running')).rejects.toMatchObject({
code: 'DIAGNOSTICS_UNAVAILABLE',
status: 502,
})
})
test('reports missing diagnostics for an unmatched instance', async () => {
const service = createSshCheckService({fetchDiagnostics: async () => ({ec2: {instances: []}})})
await expect(service.check('i-missing')).rejects.toMatchObject({code: 'DIAGNOSTICS_UNAVAILABLE', status: 404})
})
test('reports a stopped instance before attempting a connection', async () => {
const service = createSshCheckService({
fetchDiagnostics: async () => ({ec2: {instances: [
{instanceId: 'i-stopped', sshHost: '127.0.0.1', sshPort: 2222, containerStatus: 'stopped'},
]}}),
connect: async () => { throw new Error('must not connect') },
})
await expect(service.check('i-stopped')).rejects.toMatchObject({code: 'INSTANCE_NOT_RUNNING', status: 409})
})
test('rejects an invalid banner returned by a connector', async () => {
const service = createSshCheckService({
fetchDiagnostics: async () => ({ec2: {instances: [
{instanceId: 'i-running', sshHost: '127.0.0.1', sshPort: 2222, containerStatus: 'running'},
]}}),
connect: async () => 'not ssh',
})
await expect(service.check('i-running')).rejects.toBeInstanceOf(SshCheckError)
await expect(service.check('i-running')).rejects.toMatchObject({code: 'SSH_BANNER_MISSING'})
})
})
+194
View File
@@ -0,0 +1,194 @@
import {createConnection} from 'node:net'
const DEFAULT_TIMEOUT_MS = 3_000
const MAX_BANNER_BYTES = 8_192
type Ec2DiagnosticInstance = {
instanceId?: string
sshHost?: string
sshPort?: number
containerStatus?: 'running' | 'stopped' | 'unavailable'
}
type DiagnosticResponse = {
ec2?: {instances?: Ec2DiagnosticInstance[]}
}
export type SshTarget = {
host: string
port: number
}
export type SshCheckResult = {
ok: true
banner: string
checkedFrom: 'UI backend'
checkedAt: string
target: SshTarget
}
export type SshCheckErrorCode =
| 'DIAGNOSTICS_UNAVAILABLE'
| 'INSTANCE_NOT_RUNNING'
| 'CONNECTION_REFUSED'
| 'CONNECTION_TIMEOUT'
| 'SSH_BANNER_MISSING'
export class SshCheckError extends Error {
constructor(
readonly code: SshCheckErrorCode,
message: string,
readonly status: 404 | 409 | 502 | 503 | 504,
) {
super(message)
this.name = 'SshCheckError'
}
}
export type DiagnosticFetcher = () => Promise<DiagnosticResponse>
export type SshSocketConnector = (target: SshTarget, timeoutMs: number) => Promise<string>
type SshCheckDependencies = {
fetchDiagnostics?: DiagnosticFetcher
connect?: SshSocketConnector
loopbackHost?: string
now?: () => Date
timeoutMs?: number
}
async function fetchDiagnostics(): Promise<DiagnosticResponse> {
const endpoint = process.env.FLOCI_ENDPOINT
if (!endpoint) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics are unavailable', 503)
}
let response: Response
try {
response = await fetch(`${endpoint}/_floci/diagnose`, {signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)})
} catch {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics are unavailable', 503)
}
if (!response.ok) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics are unavailable', 503)
}
try {
return await response.json() as DiagnosticResponse
} catch {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics returned an invalid response', 502)
}
}
export function readSshBanner(target: SshTarget, timeoutMs: number): Promise<string> {
return new Promise((resolve, reject) => {
const socket = createConnection(target)
let settled = false
let buffer = Buffer.alloc(0)
const finish = (error?: SshCheckError, banner?: string) => {
if (settled) return
settled = true
clearTimeout(timer)
socket.destroy()
if (error) reject(error)
else resolve(banner!)
}
const timer = setTimeout(() => {
finish(new SshCheckError('CONNECTION_TIMEOUT', `SSH connection to ${target.host}:${target.port} timed out`, 504))
}, timeoutMs)
socket.on('data', (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk])
if (buffer.length > MAX_BANNER_BYTES) {
finish(new SshCheckError('SSH_BANNER_MISSING', 'SSH server did not send a valid banner', 502))
return
}
const lines = buffer.toString('utf8').split(/\r?\n/)
for (let index = 0; index < lines.length - 1; index++) {
const line = lines[index]
if (/^SSH-\d+\.\d+-[\x21-\x7e]+(?: [\x20-\x7e]*)?$/.test(line)
&& Buffer.byteLength(line, 'ascii') <= 253) {
finish(undefined, line)
return
}
}
})
socket.on('end', () => {
finish(new SshCheckError('SSH_BANNER_MISSING', 'SSH server closed without sending a valid banner', 502))
})
socket.on('close', () => {
finish(new SshCheckError('SSH_BANNER_MISSING', 'SSH server closed without sending a valid banner', 502))
})
socket.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'ECONNREFUSED') {
finish(new SshCheckError('CONNECTION_REFUSED', `SSH connection to ${target.host}:${target.port} was refused`, 503))
return
}
if (error.code === 'ETIMEDOUT') {
finish(new SshCheckError('CONNECTION_TIMEOUT', `SSH connection to ${target.host}:${target.port} timed out`, 504))
return
}
finish(new SshCheckError('CONNECTION_REFUSED', `SSH connection to ${target.host}:${target.port} failed`, 503))
})
})
}
export function createSshCheckService(dependencies: SshCheckDependencies = {}) {
const diagnosticFetcher = dependencies.fetchDiagnostics ?? fetchDiagnostics
const connector = dependencies.connect ?? readSshBanner
const sshHost = dependencies.loopbackHost || process.env.FLOCI_EC2_SSH_HOST || 'localhost'
const now = dependencies.now ?? (() => new Date())
const timeoutMs = dependencies.timeoutMs ?? DEFAULT_TIMEOUT_MS
return {
async check(instanceId: string): Promise<SshCheckResult> {
let diagnostics: DiagnosticResponse
try {
diagnostics = await diagnosticFetcher()
} catch (error) {
if (error instanceof SshCheckError) throw error
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics are unavailable', 503)
}
if (!diagnostics || typeof diagnostics !== 'object'
|| !Array.isArray(diagnostics.ec2?.instances)) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', 'Floci diagnostics returned an invalid response', 502)
}
const instance = diagnostics.ec2.instances.find((candidate) => candidate.instanceId === instanceId)
if (!instance) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', `SSH diagnostics for instance ${instanceId} are unavailable`, 404)
}
if (!instance.containerStatus) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', `SSH diagnostics for instance ${instanceId} are unavailable`, 404)
}
if (instance.containerStatus !== 'running') {
throw new SshCheckError('INSTANCE_NOT_RUNNING', `Instance ${instanceId} is not running`, 409)
}
if (typeof instance.sshPort !== 'number' || !Number.isInteger(instance.sshPort)
|| instance.sshPort < 1 || instance.sshPort > 65_535) {
throw new SshCheckError('DIAGNOSTICS_UNAVAILABLE', `SSH diagnostics for instance ${instanceId} are unavailable`, 404)
}
const target = {
host: sshHost,
port: instance.sshPort,
}
const banner = await connector(target, timeoutMs)
if (!/^SSH-\d+\.\d+-/.test(banner)) {
throw new SshCheckError('SSH_BANNER_MISSING', 'SSH server did not send a valid banner', 502)
}
return {
ok: true,
banner,
checkedFrom: 'UI backend',
checkedAt: now().toISOString(),
target,
}
},
}
}
export const sshCheckService = createSshCheckService()