CI/CD - CodeBuild + CodeDeploy + CodePipeline

This commit is contained in:
Zakaria
2026-10-01 09:18:04 -04:00
parent aa02cdb7f6
commit 7f0d707578
41 changed files with 1817 additions and 78 deletions
@@ -1,6 +1,7 @@
import {ValidationError} from '../cloud-spi/errors'
import {ListTagsForResourceCommand, type ModifyDBInstanceCommandInput, type RDSClient} from '@aws-sdk/client-rds'
import {rds as defaultRds} from '../aws'
import {ExecuteStatementCommand, type Field, type RDSDataClient} from '@aws-sdk/client-rds-data'
import {rds as defaultRds, awsClients} from '../aws'
import {awsDatabaseSchema} from '../cloud-spi/databaseSchema'
import type {
CloudResource,
@@ -10,13 +11,17 @@ import type {
DatabaseSnapshot,
ResourceQuery,
ServiceSchema,
SqlConnectionInput,
SqlDatabase,
SqlQueryResult,
SqlTable,
UpdateResourceInput,
} from '../cloud-spi/types'
import {rdsService, type RdsInstance, type RdsSnapshot} from '../services/rds'
type RdsServiceShape = Pick<
typeof rdsService,
'listInstances' | 'describeInstance' | 'createInstance' | 'modifyInstance' | 'deleteInstance' | 'listSnapshots' | 'createSnapshot' | 'listOrderableInstanceClasses'
'listInstances' | 'describeInstance' | 'createInstance' | 'modifyInstance' | 'deleteInstance' | 'startInstance' | 'stopInstance' | 'rebootInstance' | 'listSnapshots' | 'createSnapshot' | 'listOrderableInstanceClasses'
>
export class AwsDatabaseAdapter implements CloudServiceAdapter {
@@ -26,6 +31,7 @@ export class AwsDatabaseAdapter implements CloudServiceAdapter {
constructor(
private readonly rdsService_: RdsServiceShape = rdsService,
private readonly rds: RDSClient = defaultRds,
private readonly rdsData: RDSDataClient = awsClients.rdsData,
) {}
schema(): ServiceSchema {
@@ -118,6 +124,18 @@ export class AwsDatabaseAdapter implements CloudServiceAdapter {
await this.rdsService_.deleteInstance(id)
}
async start(id: string): Promise<void> {
await this.rdsService_.startInstance(id)
}
async stop(id: string): Promise<void> {
await this.rdsService_.stopInstance(id)
}
async reboot(id: string): Promise<void> {
await this.rdsService_.rebootInstance(id)
}
async listDatabaseSnapshots(instanceIdentifier?: string): Promise<DatabaseSnapshot[]> {
const normalizedIdentifier = stringValue(instanceIdentifier) || undefined
return (await this.rdsService_.listSnapshots(normalizedIdentifier)).map(snapshotToDatabaseSnapshot)
@@ -136,6 +154,73 @@ export class AwsDatabaseAdapter implements CloudServiceAdapter {
return await this.rdsService_.listOrderableInstanceClasses(normalizedEngine)
}
async listSqlDatabases(serverId: string, connection: SqlConnectionInput): Promise<SqlDatabase[]> {
const instance = await this.rdsService_.describeInstance(serverId)
const engine = sqlEngine(instance.engine)
const result = await this.execute(instance, defaultDatabase(instance, engine), databaseListQuery(engine))
return (result.resultSets[0]?.rows ?? []).flatMap((row) => {
const name = stringValue(row.name)
if (!name) return []
return [{
name,
state: stringValue(row.state) || 'ONLINE',
createdAt: null,
isSystem: row.isSystem === true,
}]
})
}
async listSqlTables(serverId: string, connection: SqlConnectionInput): Promise<SqlTable[]> {
const instance = await this.rdsService_.describeInstance(serverId)
const engine = sqlEngine(instance.engine)
const database = sqlDatabase(connection, instance, engine)
const result = await this.execute(instance, database, tableListQuery(engine))
return (result.resultSets[0]?.rows ?? []).flatMap((row) => {
const schema = stringValue(row.schemaName)
const name = stringValue(row.name)
if (!schema || !name) return []
return [{
schema,
name,
type: row.objectType === 'view' ? 'view' as const : 'table' as const,
rowCount: integerValue(row.rowCount),
}]
})
}
async querySql(serverId: string, connection: SqlConnectionInput, query: string): Promise<SqlQueryResult> {
if (!query.trim()) throw new ValidationError('SQL query is required')
const instance = await this.rdsService_.describeInstance(serverId)
const engine = sqlEngine(instance.engine)
return this.execute(instance, sqlDatabase(connection, instance, engine), query)
}
private async execute(instance: RdsInstance, database: string, sql: string): Promise<SqlQueryResult> {
if (!instance.arn) throw new ValidationError('DB instance ARN is not available')
const startedAt = performance.now()
const response = await this.rdsData.send(new ExecuteStatementCommand({
resourceArn: instance.arn,
// Floci falls back to the RDS instance credentials when this local
// placeholder does not resolve to a Secrets Manager secret.
secretArn: 'arn:aws:secretsmanager:us-east-1:000000000000:secret:floci-ui-rds-data',
database,
sql,
includeResultMetadata: true,
}))
const columns = (response.columnMetadata ?? []).map((column, index) => ({
name: column.name ?? `column_${index + 1}`,
type: String(column.typeName ?? column.type ?? 'unknown'),
}))
const rows = (response.records ?? []).map((record) => Object.fromEntries(
record.map((field, index) => [columns[index]?.name ?? `column_${index + 1}`, fieldValue(field)]),
))
return {
resultSets: columns.length > 0 ? [{columns, rows, truncated: false}] : [],
rowsAffected: [response.numberOfRecordsUpdated ?? 0],
durationMs: Math.round(performance.now() - startedAt),
}
}
private async toResource(instance: RdsInstance): Promise<CloudResource> {
const tags = instance.arn ? await this.getTags(instance.arn) : []
const vpcSecurityGroupIds = (instance.vpcSecurityGroups ?? [])
@@ -192,6 +277,75 @@ export class AwsDatabaseAdapter implements CloudServiceAdapter {
}
}
const POSTGRES_DATABASES_QUERY = `
SELECT datname AS name,
CASE WHEN datallowconn THEN 'ONLINE' ELSE 'OFFLINE' END AS state,
datname IN ('postgres', 'template0', 'template1') AS "isSystem"
FROM pg_database
WHERE datallowconn
ORDER BY "isSystem" DESC, datname`
const POSTGRES_TABLES_QUERY = `
SELECT namespace.nspname AS "schemaName", relation.relname AS name,
CASE WHEN relation.relkind IN ('v', 'm') THEN 'view' ELSE 'table' END AS "objectType",
CASE WHEN relation.relkind IN ('r', 'p') THEN relation.reltuples::bigint ELSE NULL END AS "rowCount"
FROM pg_class AS relation
INNER JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace
WHERE namespace.nspname NOT IN ('pg_catalog', 'information_schema')
AND namespace.nspname NOT LIKE 'pg_toast%'
AND relation.relkind IN ('r', 'p', 'v', 'm')
ORDER BY namespace.nspname, relation.relname`
const MYSQL_DATABASES_QUERY = `
SELECT schema_name AS name, 'ONLINE' AS state,
schema_name IN ('information_schema', 'mysql', 'performance_schema', 'sys') AS isSystem
FROM information_schema.schemata
ORDER BY isSystem DESC, schema_name`
const MYSQL_TABLES_QUERY = `
SELECT table_schema AS schemaName, table_name AS name,
CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS objectType,
table_rows AS rowCount
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY table_name`
function sqlEngine(engine: string | undefined): 'postgresql' | 'mysql' {
if (engine === 'postgres') return 'postgresql'
if (engine === 'mysql') return 'mysql'
throw new ValidationError('SQL exploration is available only for PostgreSQL and MySQL RDS instances')
}
function defaultDatabase(instance: RdsInstance, engine: 'postgresql' | 'mysql'): string {
return instance.dbName || (engine === 'postgresql' ? 'postgres' : 'mysql')
}
function sqlDatabase(connection: SqlConnectionInput, instance: RdsInstance, engine: 'postgresql' | 'mysql'): string {
return stringValue(connection.database) || defaultDatabase(instance, engine)
}
function databaseListQuery(engine: 'postgresql' | 'mysql'): string {
return engine === 'postgresql' ? POSTGRES_DATABASES_QUERY : MYSQL_DATABASES_QUERY
}
function tableListQuery(engine: 'postgresql' | 'mysql'): string {
return engine === 'postgresql' ? POSTGRES_TABLES_QUERY : MYSQL_TABLES_QUERY
}
function integerValue(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value)
if (typeof value === 'string' && /^-?\d+$/.test(value)) return Number(value)
return null
}
function fieldValue(field: Field): unknown {
if (field.isNull) return null
if (field.stringValue !== undefined) return field.stringValue
if (field.longValue !== undefined) return Number(field.longValue)
if (field.doubleValue !== undefined) return field.doubleValue
if (field.booleanValue !== undefined) return field.booleanValue
if (field.blobValue !== undefined) return Buffer.from(field.blobValue).toString('base64')
if (field.arrayValue !== undefined) return field.arrayValue
return null
}
const INSTANCE_IDENTIFIER_PATTERN = /^(?!.*--)[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/
const SNAPSHOT_IDENTIFIER_PATTERN = /^(?!.*--)[A-Za-z](?:[A-Za-z0-9-]{0,253}[A-Za-z0-9])?$/