From 8631966cc004edce1a2076c41d1ad376a9dbaba1 Mon Sep 17 00:00:00 2001 From: allardy Date: Fri, 13 Feb 2026 19:50:51 -0500 Subject: [PATCH 1/4] chore: run prettier --- .github/workflows/test.yml | 2 +- .prettierignore | 23 ++ .prettierrc | 9 + cdk-postgresql/README.md | 64 ++-- cdk-postgresql/jest.config.js | 10 +- cdk-postgresql/lib/database.handler.ts | 173 +++++----- cdk-postgresql/lib/database.ts | 34 +- cdk-postgresql/lib/handler.ts | 20 +- cdk-postgresql/lib/index.ts | 6 +- cdk-postgresql/lib/lambda.types.ts | 114 +++--- cdk-postgresql/lib/postgres.ts | 49 +-- cdk-postgresql/lib/provider.ts | 121 +++---- cdk-postgresql/lib/role.handler.ts | 199 +++++------ cdk-postgresql/lib/role.ts | 38 +- cdk-postgresql/lib/util.ts | 134 ++++---- cdk-postgresql/test/constructs.test.ts | 206 ++++++----- cdk-postgresql/test/helpers.ts | 83 ++--- cdk-postgresql/test/lambda.test.ts | 459 ++++++++++++------------- cdk-postgresql/test/util.test.ts | 18 +- package.json | 5 +- yarn.lock | 18 + 21 files changed, 873 insertions(+), 912 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b6be8a..401f070 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v2.1.0 - uses: actions/setup-node@v1 with: - node-version: "16.0.0" + node-version: '16.0.0' - name: Install working-directory: ./cdk-postgresql run: | diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e07a4c8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,23 @@ +# based on https://gist.github.com/thinkricardo/74f37d82b686de371b0853a5d66d559c + +# ignore all files +* + +# include all folders +!**/ + +# include files to format +!*.yaml +!*.yml +!*.md +pnpm-lock.yaml + +# exclusions +**/*.d.ts +**/cdk.out/**/* +**/*.gomplate.yaml +**/README.md +cloud/lib/docker-image/traefik/plugins/htransformation/**/* + +# cdktf +**/.gen/** diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6877195 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "none", + "semi": false, + "bracketSpacing": true, + "requirePragma": false, + "arrowParens": "always" +} diff --git a/cdk-postgresql/README.md b/cdk-postgresql/README.md index 4e8b553..db334ed 100644 --- a/cdk-postgresql/README.md +++ b/cdk-postgresql/README.md @@ -13,27 +13,27 @@ A `Provider` instance is required in order to establish a connection to your Postgresql instance ```typescript -const theMasterSecret: secretsmanager.ISecret; +const theMasterSecret: secretsmanager.ISecret // you can connect to to a publicly available instance -const provider = new Provider(this, "Provider", { - host: "your.db.host.net", - username: "master", +const provider = new Provider(this, 'Provider', { + host: 'your.db.host.net', + username: 'master', password: theMasterSecret, port: 5432, vpc, - securityGroups: [dbClusterSecurityGroup], -}); + securityGroups: [dbClusterSecurityGroup] +}) // or a private instance in your VPC -const provider = new Provider(this, "Provider", { - host: "your.db.host.net", - username: "master", +const provider = new Provider(this, 'Provider', { + host: 'your.db.host.net', + username: 'master', password: theMasterSecret, port: 5432, vpc, - securityGroups: [yourDatabaseSecurityGroup], -}); + securityGroups: [yourDatabaseSecurityGroup] +}) ``` You can reuse the same `Provider` instance when creating your different `Role` and `Database` instances. @@ -41,28 +41,28 @@ You can reuse the same `Provider` instance when creating your different `Role` a ### Database ```typescript -import { Database } from "@botpress/cdk-postgresql"; +import { Database } from '@botpress/cdk-postgresql' -const db = new Database(this, "Database", { +const db = new Database(this, 'Database', { provider, - name: "mynewdb", - owner: "somerole", - removalPolicy: cdk.RemovalPolicy.RETAIN, // default is RETAIN -}); + name: 'mynewdb', + owner: 'somerole', + removalPolicy: cdk.RemovalPolicy.RETAIN // default is RETAIN +}) ``` ### Role ```typescript -import { Role } from "@botpress/cdk-postgresql"; +import { Role } from '@botpress/cdk-postgresql' -const rolePassword: secretsmanager.ISecret; -const role = new Role(this, "Role", { +const rolePassword: secretsmanager.ISecret +const role = new Role(this, 'Role', { provider, - name: "newrole", + name: 'newrole', password: rolePassword, - removalPolicy: cdk.RemovalPolicy.RETAIN, // Default is DESTROY -}); + removalPolicy: cdk.RemovalPolicy.RETAIN // Default is DESTROY +}) ``` ## Tips @@ -72,17 +72,17 @@ const role = new Role(this, "Role", { In many cases, you want to create a `Role` and use that role as the `Database` owner. You can achieve this by adding an [explicit dependency](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.html#dependencies) between the two instances: ```typescript -const roleName = "newRole"; -const role = new Role(this, "Role", { +const roleName = 'newRole' +const role = new Role(this, 'Role', { provider, name: roleName, - password: rolePassword, -}); -const db = new Database(this, "Database", { + password: rolePassword +}) +const db = new Database(this, 'Database', { provider, - name: "mydb", - owner: roleName, -}); + name: 'mydb', + owner: roleName +}) -db.node.addDependency(role); +db.node.addDependency(role) ``` diff --git a/cdk-postgresql/jest.config.js b/cdk-postgresql/jest.config.js index 3478e83..fe1b489 100644 --- a/cdk-postgresql/jest.config.js +++ b/cdk-postgresql/jest.config.js @@ -1,7 +1,7 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - preset: "ts-jest", - testEnvironment: "node", - roots: ["/test"], - testMatch: ["**/*.test.ts"], -}; + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'] +} diff --git a/cdk-postgresql/lib/database.handler.ts b/cdk-postgresql/lib/database.handler.ts index 360a3b0..6b9b162 100644 --- a/cdk-postgresql/lib/database.handler.ts +++ b/cdk-postgresql/lib/database.handler.ts @@ -1,141 +1,126 @@ -import format from "pg-format"; -import { getConnectedClient, validateConnection, hashCode } from "./util"; -import * as postgres from "./postgres"; +import format from 'pg-format' +import { getConnectedClient, validateConnection, hashCode } from './util' +import * as postgres from './postgres' import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceUpdateEvent, - CloudFormationCustomResourceDeleteEvent, -} from "aws-lambda/trigger/cloudformation-custom-resource"; -import { Connection } from "./lambda.types"; + CloudFormationCustomResourceDeleteEvent +} from 'aws-lambda/trigger/cloudformation-custom-resource' +import { Connection } from './lambda.types' interface Props { - ServiceToken: string; - Connection: Connection; - Name: string; - Owner: string; + ServiceToken: string + Connection: Connection + Name: string + Owner: string } export const handler = async (event: CloudFormationCustomResourceEvent) => { switch (event.RequestType) { - case "Create": - return handleCreate(event); - case "Update": - return handleUpdate(event); - case "Delete": - return handleDelete(event); + case 'Create': + return handleCreate(event) + case 'Update': + return handleUpdate(event) + case 'Delete': + return handleDelete(event) } -}; +} const generatePhysicalId = (props: Props): string => { - const { Host, Port } = props.Connection; - const suffix = Math.abs(hashCode(`${Host}-${Port}`)); - return `${props.Name}-${suffix}`; -}; + const { Host, Port } = props.Connection + const suffix = Math.abs(hashCode(`${Host}-${Port}`)) + return `${props.Name}-${suffix}` +} const handleCreate = async (event: CloudFormationCustomResourceCreateEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); + const props = event.ResourceProperties as Props + validateProps(props) await createDatabase({ connection: props.Connection, name: props.Name, - owner: props.Owner, - }); + owner: props.Owner + }) return { - PhysicalResourceId: generatePhysicalId(props), - }; -}; + PhysicalResourceId: generatePhysicalId(props) + } +} const handleUpdate = async (event: CloudFormationCustomResourceUpdateEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); - const oldProps = event.OldResourceProperties as Props; + const props = event.ResourceProperties as Props + validateProps(props) + const oldProps = event.OldResourceProperties as Props - const oldPhysicalResourceId = generatePhysicalId(oldProps); - const physicalResourceId = generatePhysicalId(props); + const oldPhysicalResourceId = generatePhysicalId(oldProps) + const physicalResourceId = generatePhysicalId(props) if (physicalResourceId != oldPhysicalResourceId) { await createDatabase({ connection: props.Connection, name: props.Name, - owner: props.Owner, - }); - return { PhysicalResourceId: physicalResourceId }; + owner: props.Owner + }) + return { PhysicalResourceId: physicalResourceId } } if (props.Owner != oldProps.Owner) { - await updateDbOwner(props.Connection, props.Name, props.Owner); + await updateDbOwner(props.Connection, props.Name, props.Owner) } - return { PhysicalResourceId: physicalResourceId }; -}; + return { PhysicalResourceId: physicalResourceId } +} const handleDelete = async (event: CloudFormationCustomResourceDeleteEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); - await deleteDatabase(props.Connection, props.Name, props.Owner); - return {}; -}; + const props = event.ResourceProperties as Props + validateProps(props) + await deleteDatabase(props.Connection, props.Name, props.Owner) + return {} +} const validateProps = (props: Props) => { - if (!("Connection" in props)) { - throw "Connection property is required"; + if (!('Connection' in props)) { + throw 'Connection property is required' } - validateConnection(props.Connection); + validateConnection(props.Connection) - if (!("Name" in props)) { - throw "Name property is required"; + if (!('Name' in props)) { + throw 'Name property is required' } - if (!("Owner" in props)) { - throw "Owner property is required"; + if (!('Owner' in props)) { + throw 'Owner property is required' } -}; - -export const createDatabase = async (props: { - connection: Connection; - name: string; - owner: string; -}) => { - const { connection, name, owner } = props; - console.log("Creating database", name); - const client = await getConnectedClient(connection); - - await postgres.createDatabase({ client, name, owner }); - await client.end(); - console.log("Created database"); -}; - -export const deleteDatabase = async ( - connection: Connection, - name: string, - owner: string -) => { - console.log("Deleting database", name); - const client = await getConnectedClient(connection); +} + +export const createDatabase = async (props: { connection: Connection; name: string; owner: string }) => { + const { connection, name, owner } = props + console.log('Creating database', name) + const client = await getConnectedClient(connection) + + await postgres.createDatabase({ client, name, owner }) + await client.end() + console.log('Created database') +} + +export const deleteDatabase = async (connection: Connection, name: string, owner: string) => { + console.log('Deleting database', name) + const client = await getConnectedClient(connection) // First, drop all remaining DB connections // Sometimes, DB connections are still alive even though the ECS service has been deleted await client.query( - format( - "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname=%L", - name - ) - ); + format('SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname=%L', name) + ) // Then, drop the DB - await client.query(format("DROP DATABASE %I", name)); + await client.query(format('DROP DATABASE %I', name)) // await client.query(format("REVOKE %I FROM %I", owner, connection.Username)); - await client.end(); -}; - -export const updateDbOwner = async ( - connection: Connection, - name: string, - owner: string -) => { - console.log(`Updating DB ${name} owner to ${owner}`); - const client = await getConnectedClient(connection); - - await client.query(format("ALTER DATABASE %I OWNER TO %I", name, owner)); - await client.end(); -}; + await client.end() +} + +export const updateDbOwner = async (connection: Connection, name: string, owner: string) => { + console.log(`Updating DB ${name} owner to ${owner}`) + const client = await getConnectedClient(connection) + + await client.query(format('ALTER DATABASE %I OWNER TO %I', name, owner)) + await client.end() +} diff --git a/cdk-postgresql/lib/database.ts b/cdk-postgresql/lib/database.ts index d8e22be..7f647c5 100644 --- a/cdk-postgresql/lib/database.ts +++ b/cdk-postgresql/lib/database.ts @@ -1,50 +1,50 @@ -import * as cdk from "aws-cdk-lib"; -import { RemovalPolicy } from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { Provider } from "./provider"; +import * as cdk from 'aws-cdk-lib' +import { RemovalPolicy } from 'aws-cdk-lib' +import { Construct } from 'constructs' +import { Provider } from './provider' export interface DatabaseProps { /** * Provider required to connect to the Postgresql server */ - provider: Provider; + provider: Provider /** * The name of the database. Must be unique on the PostgreSQL server instance where it is configured. */ - name: string; + name: string /** * The role name of the user who will own the database */ - owner: string; + owner: string /** * Policy to apply when the database is removed from this stack. * * @default - The database will be orphaned. */ - removalPolicy?: RemovalPolicy; + removalPolicy?: RemovalPolicy } export class Database extends Construct { constructor(scope: Construct, id: string, props: DatabaseProps) { - super(scope, id); + super(scope, id) - const { provider, name, owner, removalPolicy } = props; + const { provider, name, owner, removalPolicy } = props - const cr = new cdk.CustomResource(this, "CustomResource", { + const cr = new cdk.CustomResource(this, 'CustomResource', { serviceToken: provider.serviceToken, - resourceType: "Custom::Postgresql-Database", + resourceType: 'Custom::Postgresql-Database', properties: { connection: provider.buildConnectionProperty(), name, - owner, + owner }, - pascalCaseProperties: true, - }); + pascalCaseProperties: true + }) - cr.applyRemovalPolicy(removalPolicy || cdk.RemovalPolicy.RETAIN); - cr.node.addDependency(provider); + cr.applyRemovalPolicy(removalPolicy || cdk.RemovalPolicy.RETAIN) + cr.node.addDependency(provider) } } diff --git a/cdk-postgresql/lib/handler.ts b/cdk-postgresql/lib/handler.ts index cfb0dfc..83ba4be 100644 --- a/cdk-postgresql/lib/handler.ts +++ b/cdk-postgresql/lib/handler.ts @@ -1,16 +1,16 @@ -import { CloudFormationCustomResourceEvent } from "aws-lambda/trigger/cloudformation-custom-resource"; +import { CloudFormationCustomResourceEvent } from 'aws-lambda/trigger/cloudformation-custom-resource' -import { VError } from "verror"; -import { handler as dbHandler } from "./database.handler"; -import { handler as roleHandler } from "./role.handler"; +import { VError } from 'verror' +import { handler as dbHandler } from './database.handler' +import { handler as roleHandler } from './role.handler' export const handler = async (event: CloudFormationCustomResourceEvent) => { switch (event.ResourceType) { - case "Custom::Postgresql-Role": - return roleHandler(event); - case "Custom::Postgresql-Database": - return dbHandler(event); + case 'Custom::Postgresql-Role': + return roleHandler(event) + case 'Custom::Postgresql-Database': + return dbHandler(event) default: - throw new VError(`unexpected ResourceType: ${event.ResourceType}`); + throw new VError(`unexpected ResourceType: ${event.ResourceType}`) } -}; +} diff --git a/cdk-postgresql/lib/index.ts b/cdk-postgresql/lib/index.ts index 147c9b8..c547d98 100644 --- a/cdk-postgresql/lib/index.ts +++ b/cdk-postgresql/lib/index.ts @@ -1,3 +1,3 @@ -export * from "./database"; -export * from "./role"; -export * from "./provider"; +export * from './database' +export * from './role' +export * from './provider' diff --git a/cdk-postgresql/lib/lambda.types.ts b/cdk-postgresql/lib/lambda.types.ts index 4bfa946..9f24f72 100644 --- a/cdk-postgresql/lib/lambda.types.ts +++ b/cdk-postgresql/lib/lambda.types.ts @@ -1,87 +1,81 @@ import { CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceDeleteEvent, - CloudFormationCustomResourceUpdateEvent, -} from "aws-lambda"; + CloudFormationCustomResourceUpdateEvent +} from 'aws-lambda' -export type SSLMode = "require" | "disable"; +export type SSLMode = 'require' | 'disable' export interface Connection { - Host: string; - Port: number; - Username: string; - PasswordArn: string; - PasswordField?: string; - Database: string; - SSLMode: SSLMode; + Host: string + Port: number + Username: string + PasswordArn: string + PasswordField?: string + Database: string + SSLMode: SSLMode } -export interface CreateDatabaseEvent - extends CloudFormationCustomResourceCreateEvent { +export interface CreateDatabaseEvent extends CloudFormationCustomResourceCreateEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - Owner: string; - }; + ServiceToken: string + Connection: Connection + Name: string + Owner: string + } } -export interface DeleteDatabaseEvent - extends CloudFormationCustomResourceDeleteEvent { +export interface DeleteDatabaseEvent extends CloudFormationCustomResourceDeleteEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - Owner: string; - }; + ServiceToken: string + Connection: Connection + Name: string + Owner: string + } } -export interface UpdateDatabaseEvent - extends CloudFormationCustomResourceUpdateEvent { +export interface UpdateDatabaseEvent extends CloudFormationCustomResourceUpdateEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - Owner: string; - }; + ServiceToken: string + Connection: Connection + Name: string + Owner: string + } OldResourceProperties: { - Connection: Connection; - Name: string; - Owner: string; - }; + Connection: Connection + Name: string + Owner: string + } } -export interface CreateRoleEvent - extends CloudFormationCustomResourceCreateEvent { +export interface CreateRoleEvent extends CloudFormationCustomResourceCreateEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - PasswordArn: string; - }; + ServiceToken: string + Connection: Connection + Name: string + PasswordArn: string + } } -export interface DeleteRoleEvent - extends CloudFormationCustomResourceDeleteEvent { +export interface DeleteRoleEvent extends CloudFormationCustomResourceDeleteEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - PasswordArn: string; - }; + ServiceToken: string + Connection: Connection + Name: string + PasswordArn: string + } } -export interface UpdateRoleEvent - extends CloudFormationCustomResourceUpdateEvent { +export interface UpdateRoleEvent extends CloudFormationCustomResourceUpdateEvent { ResourceProperties: { - ServiceToken: string; - Connection: Connection; - Name: string; - PasswordArn: string; - }; + ServiceToken: string + Connection: Connection + Name: string + PasswordArn: string + } OldResourceProperties: { - Connection: Connection; - Name: string; - PasswordArn: string; - }; + Connection: Connection + Name: string + PasswordArn: string + } } diff --git a/cdk-postgresql/lib/postgres.ts b/cdk-postgresql/lib/postgres.ts index 4e5932b..13e94d6 100644 --- a/cdk-postgresql/lib/postgres.ts +++ b/cdk-postgresql/lib/postgres.ts @@ -1,47 +1,36 @@ -import { VError } from "verror"; -import { Client, DatabaseError } from "pg"; -import format from "pg-format"; -import * as util from "util"; +import { VError } from 'verror' +import { Client, DatabaseError } from 'pg' +import format from 'pg-format' +import * as util from 'util' const isDatabaseError = (e: any): e is DatabaseError => { - return typeof e.name === "string" && typeof e.length === "number"; -}; + return typeof e.name === 'string' && typeof e.length === 'number' +} -export const createRole = async (props: { - client: Client; - name: string; - password: string; -}) => { - const { client, name, password } = props; +export const createRole = async (props: { client: Client; name: string; password: string }) => { + const { client, name, password } = props - await client.query(format("CREATE USER %I WITH PASSWORD %L", name, password)); -}; + await client.query(format('CREATE USER %I WITH PASSWORD %L', name, password)) +} -export const createDatabase = async (props: { - client: Client; - name: string; - owner: string; -}) => { - const { client, name, owner } = props; +export const createDatabase = async (props: { client: Client; name: string; owner: string }) => { + const { client, name, owner } = props try { - await client.query(format("GRANT %I TO %I", owner, client.user)); + await client.query(format('GRANT %I TO %I', owner, client.user)) } catch (e) { if (!util.types.isNativeError(e)) { - throw e; + throw e } if ( !isDatabaseError(e) || - !( - e.code === "0LP01" && - e.message === `role "${owner}" is a member of role "${client.user}"` - ) + !(e.code === '0LP01' && e.message === `role "${owner}" is a member of role "${client.user}"`) ) { - throw new VError(e, "unexpected error while creating grant"); + throw new VError(e, 'unexpected error while creating grant') } - console.warn(e.message); + console.warn(e.message) } - return client.query(format("CREATE DATABASE %I WITH OWNER %I", name, owner)); -}; + return client.query(format('CREATE DATABASE %I WITH OWNER %I', name, owner)) +} diff --git a/cdk-postgresql/lib/provider.ts b/cdk-postgresql/lib/provider.ts index e9aee12..e6cbf82 100644 --- a/cdk-postgresql/lib/provider.ts +++ b/cdk-postgresql/lib/provider.ts @@ -1,62 +1,62 @@ -import * as cdk from "aws-cdk-lib"; -import { Construct } from "constructs"; -import * as ec2 from "aws-cdk-lib/aws-ec2"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; -import * as logs from "aws-cdk-lib/aws-logs"; -import * as iam from "aws-cdk-lib/aws-iam"; -import * as cr from "aws-cdk-lib/custom-resources"; -import path from "path"; -import { Connection, SSLMode } from "./lambda.types"; +import * as cdk from 'aws-cdk-lib' +import { Construct } from 'constructs' +import * as ec2 from 'aws-cdk-lib/aws-ec2' +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' +import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs' +import * as logs from 'aws-cdk-lib/aws-logs' +import * as iam from 'aws-cdk-lib/aws-iam' +import * as cr from 'aws-cdk-lib/custom-resources' +import path from 'path' +import { Connection, SSLMode } from './lambda.types' interface ProviderProps { /** * The address for the server connection */ - host: string; + host: string /** * The port for the server connection * * @default - 5432 */ - port?: number; + port?: number /** * Database to connect to * * @default - "postgres" */ - database?: string; + database?: string /** * Username for the server connection */ - username: string; + username: string /** * Password for the server connection */ - password: secretsmanager.ISecret; + password: secretsmanager.ISecret /** * Field to get from the password, in case the password is a object */ - passwordField?: string; + passwordField?: string /** * Set the priority for an SSL connection to the server * * @default - "require" */ - sslMode?: "require" | "disable"; + sslMode?: 'require' | 'disable' /** * VPC network to place the Provider Lambda network interfaces * * @default - "Provider is not placed within a VPC" */ - vpc?: ec2.IVpc; + vpc?: ec2.IVpc /** * The Provider Lambda will be granted inbound access @@ -64,68 +64,59 @@ interface ProviderProps { * * @default - "The Lambda has no access to any Security Groups" */ - securityGroups?: ec2.ISecurityGroup[]; + securityGroups?: ec2.ISecurityGroup[] } export class Provider extends Construct implements iam.IGrantable { - readonly grantPrincipal: cdk.aws_iam.IPrincipal; - readonly serviceToken: string; + readonly grantPrincipal: cdk.aws_iam.IPrincipal + readonly serviceToken: string - private readonly host: string; - private readonly port: number; - private readonly username: string; - private readonly database: string; - private readonly sslMode: SSLMode; - private readonly password: secretsmanager.ISecret; - private readonly passwordField?: string; + private readonly host: string + private readonly port: number + private readonly username: string + private readonly database: string + private readonly sslMode: SSLMode + private readonly password: secretsmanager.ISecret + private readonly passwordField?: string constructor(scope: Construct, id: string, props: ProviderProps) { - super(scope, id); - - const { vpc, securityGroups } = props; - - this.host = props.host; - this.username = props.username; - this.port = props.port || 5432; - this.database = props.database || "postgres"; - this.sslMode = props.sslMode || "require"; - this.password = props.password; - this.passwordField = props.passwordField; - - const handlerSecurityGroup = vpc - ? new ec2.SecurityGroup(this, "HandlerSecurityGroup", { vpc }) - : undefined; - const handlerSecurityGroups = handlerSecurityGroup - ? [handlerSecurityGroup] - : undefined; - const handler = new lambda.NodejsFunction(scope, "handler", { - entry: path.join(__dirname, "handler.js"), + super(scope, id) + + const { vpc, securityGroups } = props + + this.host = props.host + this.username = props.username + this.port = props.port || 5432 + this.database = props.database || 'postgres' + this.sslMode = props.sslMode || 'require' + this.password = props.password + this.passwordField = props.passwordField + + const handlerSecurityGroup = vpc ? new ec2.SecurityGroup(this, 'HandlerSecurityGroup', { vpc }) : undefined + const handlerSecurityGroups = handlerSecurityGroup ? [handlerSecurityGroup] : undefined + const handler = new lambda.NodejsFunction(scope, 'handler', { + entry: path.join(__dirname, 'handler.js'), bundling: { - nodeModules: ["pg", "pg-format"], + nodeModules: ['pg', 'pg-format'] }, logRetention: logs.RetentionDays.ONE_MONTH, timeout: cdk.Duration.minutes(15), vpc, - securityGroups: handlerSecurityGroups, - }); - this.grantPrincipal = handler.grantPrincipal; + securityGroups: handlerSecurityGroups + }) + this.grantPrincipal = handler.grantPrincipal - this.password.grantRead(handler); + this.password.grantRead(handler) - const provider = new cr.Provider(scope, "cr-provider", { + const provider = new cr.Provider(scope, 'cr-provider', { onEventHandler: handler, - logRetention: logs.RetentionDays.ONE_MONTH, - }); - this.serviceToken = provider.serviceToken; + logRetention: logs.RetentionDays.ONE_MONTH + }) + this.serviceToken = provider.serviceToken if (securityGroups && handlerSecurityGroup) { for (const s of securityGroups) { - s.addIngressRule( - handlerSecurityGroup, - ec2.Port.tcp(this.port), - "cdk-postgresql provider", - true - ); + s.addIngressRule(handlerSecurityGroup, ec2.Port.tcp(this.port), 'cdk-postgresql provider', true) } } } @@ -138,7 +129,7 @@ export class Provider extends Construct implements iam.IGrantable { Database: this.database, SSLMode: this.sslMode, PasswordArn: this.password.secretArn, - PasswordField: this.passwordField, - }; + PasswordField: this.passwordField + } } } diff --git a/cdk-postgresql/lib/role.handler.ts b/cdk-postgresql/lib/role.handler.ts index 818fb99..ed79dd2 100644 --- a/cdk-postgresql/lib/role.handler.ts +++ b/cdk-postgresql/lib/role.handler.ts @@ -1,167 +1,150 @@ -import format from "pg-format"; +import format from 'pg-format' import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceUpdateEvent, - CloudFormationCustomResourceDeleteEvent, -} from "aws-lambda/trigger/cloudformation-custom-resource"; + CloudFormationCustomResourceDeleteEvent +} from 'aws-lambda/trigger/cloudformation-custom-resource' -import { - validateConnection, - hashCode, - getConnectedClient, - secretsmanager, -} from "./util"; -import { Connection } from "./lambda.types"; -import * as postgres from "./postgres"; +import { validateConnection, hashCode, getConnectedClient, secretsmanager } from './util' +import { Connection } from './lambda.types' +import * as postgres from './postgres' interface Props { - ServiceToken: string; - Connection: Connection; - Name: string; - PasswordArn: string; + ServiceToken: string + Connection: Connection + Name: string + PasswordArn: string } export const handler = async (event: CloudFormationCustomResourceEvent) => { switch (event.RequestType) { - case "Create": - return handleCreate(event); - case "Update": - return handleUpdate(event); - case "Delete": - return handleDelete(event); + case 'Create': + return handleCreate(event) + case 'Update': + return handleUpdate(event) + case 'Delete': + return handleDelete(event) } -}; +} const handleCreate = async (event: CloudFormationCustomResourceCreateEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); + const props = event.ResourceProperties as Props + validateProps(props) await createRole({ connection: props.Connection, name: props.Name, - passwordArn: props.PasswordArn, - }); + passwordArn: props.PasswordArn + }) return { - PhysicalResourceId: generatePhysicalId(props), - }; -}; + PhysicalResourceId: generatePhysicalId(props) + } +} const handleUpdate = async (event: CloudFormationCustomResourceUpdateEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); + const props = event.ResourceProperties as Props + validateProps(props) - const oldProps = event.OldResourceProperties as Props; + const oldProps = event.OldResourceProperties as Props - const oldPhysicalResourceId = generatePhysicalId(oldProps); - const physicalResourceId = generatePhysicalId(props); + const oldPhysicalResourceId = generatePhysicalId(oldProps) + const physicalResourceId = generatePhysicalId(props) if (physicalResourceId != oldPhysicalResourceId) { await createRole({ connection: props.Connection, name: props.Name, - passwordArn: props.PasswordArn, - }); - return { PhysicalResourceId: physicalResourceId }; + passwordArn: props.PasswordArn + }) + return { PhysicalResourceId: physicalResourceId } } if (props.Name != oldProps.Name) { - await updateRoleName(props.Connection, oldProps.Name, props.Name); + await updateRoleName(props.Connection, oldProps.Name, props.Name) } if (props.PasswordArn != oldProps.PasswordArn) { await updateRolePassword({ connection: props.Connection, name: props.Name, - passwordArn: props.PasswordArn, - }); + passwordArn: props.PasswordArn + }) } - return { PhysicalResourceId: physicalResourceId }; -}; + return { PhysicalResourceId: physicalResourceId } +} const handleDelete = async (event: CloudFormationCustomResourceDeleteEvent) => { - const props = event.ResourceProperties as Props; - validateProps(props); - await deleteRole(props.Connection, props.Name); - return {}; -}; + const props = event.ResourceProperties as Props + validateProps(props) + await deleteRole(props.Connection, props.Name) + return {} +} const validateProps = (props: Props) => { - if (!("Connection" in props)) { - throw "Connection property is required"; + if (!('Connection' in props)) { + throw 'Connection property is required' } - validateConnection(props.Connection); + validateConnection(props.Connection) - if (!("Name" in props)) { - throw "Name property is required"; + if (!('Name' in props)) { + throw 'Name property is required' } - if (!("PasswordArn" in props)) { - throw "PasswordArn property is required"; + if (!('PasswordArn' in props)) { + throw 'PasswordArn property is required' } -}; +} const generatePhysicalId = (props: Props): string => { - const { Host, Port } = props.Connection; - const suffix = Math.abs(hashCode(`${Host}-${Port}`)); - return `role-${suffix}`; -}; + const { Host, Port } = props.Connection + const suffix = Math.abs(hashCode(`${Host}-${Port}`)) + return `role-${suffix}` +} export const deleteRole = async (connection: Connection, name: string) => { - console.log("Deleting user", name); - const client = await getConnectedClient(connection); - - await client.query(format("DROP USER %I", name)); - await client.end(); -}; - -export const updateRoleName = async ( - connection: Connection, - oldName: string, - newName: string -) => { - console.log(`Updating role name from ${oldName} to ${newName}`); - const client = await getConnectedClient(connection); - - await client.query(format("ALTER ROLE %I RENAME TO %I", oldName, newName)); - await client.end(); -}; - -export const updateRolePassword = async (props: { - connection: Connection; - name: string; - passwordArn: string; -}) => { - const { connection, name, passwordArn } = props; - console.log("Updating user password", name); - - const client = await getConnectedClient(connection); + console.log('Deleting user', name) + const client = await getConnectedClient(connection) + + await client.query(format('DROP USER %I', name)) + await client.end() +} + +export const updateRoleName = async (connection: Connection, oldName: string, newName: string) => { + console.log(`Updating role name from ${oldName} to ${newName}`) + const client = await getConnectedClient(connection) + + await client.query(format('ALTER ROLE %I RENAME TO %I', oldName, newName)) + await client.end() +} + +export const updateRolePassword = async (props: { connection: Connection; name: string; passwordArn: string }) => { + const { connection, name, passwordArn } = props + console.log('Updating user password', name) + + const client = await getConnectedClient(connection) const { SecretString: password } = await secretsmanager.getSecretValue({ - SecretId: passwordArn, - }); - - await client.query(format("ALTER USER %I WITH PASSWORD %L", name, password)); - await client.end(); -}; - -export const createRole = async (props: { - connection: Connection; - name: string; - passwordArn: string; -}) => { - const { connection, name, passwordArn } = props; - console.log("Creating user", name); - const client = await getConnectedClient(connection); + SecretId: passwordArn + }) + + await client.query(format('ALTER USER %I WITH PASSWORD %L', name, password)) + await client.end() +} + +export const createRole = async (props: { connection: Connection; name: string; passwordArn: string }) => { + const { connection, name, passwordArn } = props + console.log('Creating user', name) + const client = await getConnectedClient(connection) const { SecretString: password } = await secretsmanager.getSecretValue({ - SecretId: passwordArn, - }); + SecretId: passwordArn + }) if (!password) { - throw new Error("could not decrypt password"); + throw new Error('could not decrypt password') } - await postgres.createRole({ client, name, password }); + await postgres.createRole({ client, name, password }) - await client.end(); -}; + await client.end() +} diff --git a/cdk-postgresql/lib/role.ts b/cdk-postgresql/lib/role.ts index c9d5d25..bec6a0c 100644 --- a/cdk-postgresql/lib/role.ts +++ b/cdk-postgresql/lib/role.ts @@ -1,53 +1,53 @@ -import { Construct } from "constructs"; -import * as cdk from "aws-cdk-lib"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import { RemovalPolicy } from "aws-cdk-lib"; -import { Provider } from "./provider"; +import { Construct } from 'constructs' +import * as cdk from 'aws-cdk-lib' +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' +import { RemovalPolicy } from 'aws-cdk-lib' +import { Provider } from './provider' export interface RoleProps { /** * Provider required to connect to the Postgresql server */ - provider: Provider; + provider: Provider /** * The name of the role. Must be unique on the PostgreSQL server instance where it is configured. */ - name: string; + name: string /** * Set the role's password */ - password: secretsmanager.ISecret; + password: secretsmanager.ISecret /** * Policy to apply when the role is removed from this stack. * * @default - The role will be destroyed. */ - removalPolicy?: RemovalPolicy; + removalPolicy?: RemovalPolicy } export class Role extends Construct { constructor(scope: Construct, id: string, props: RoleProps) { - super(scope, id); + super(scope, id) - const { provider, name, password, removalPolicy } = props; + const { provider, name, password, removalPolicy } = props - password.grantRead(provider); + password.grantRead(provider) - const cr = new cdk.CustomResource(this, "CustomResource", { + const cr = new cdk.CustomResource(this, 'CustomResource', { serviceToken: provider.serviceToken, - resourceType: "Custom::Postgresql-Role", + resourceType: 'Custom::Postgresql-Role', properties: { connection: provider.buildConnectionProperty(), name, - passwordArn: password.secretArn, + passwordArn: password.secretArn }, - pascalCaseProperties: true, - }); + pascalCaseProperties: true + }) - cr.applyRemovalPolicy(removalPolicy || cdk.RemovalPolicy.DESTROY); - cr.node.addDependency(provider); + cr.applyRemovalPolicy(removalPolicy || cdk.RemovalPolicy.DESTROY) + cr.node.addDependency(provider) } } diff --git a/cdk-postgresql/lib/util.ts b/cdk-postgresql/lib/util.ts index fe84f51..27aa163 100644 --- a/cdk-postgresql/lib/util.ts +++ b/cdk-postgresql/lib/util.ts @@ -1,133 +1,125 @@ -import { SecretsManager } from "@aws-sdk/client-secrets-manager"; -import { Client, ClientConfig } from "pg"; -import { Connection } from "./lambda.types"; +import { SecretsManager } from '@aws-sdk/client-secrets-manager' +import { Client, ClientConfig } from 'pg' +import { Connection } from './lambda.types' -export const secretsmanager = new SecretsManager({}); +export const secretsmanager = new SecretsManager({}) export const isObject = (obj: any): obj is { [key: string]: any } => { - return typeof obj === "object" && !Array.isArray(obj) && obj !== null; -}; + return typeof obj === 'object' && !Array.isArray(obj) && obj !== null +} export const getConnectedClient = async (connection: Connection) => { - console.debug( - `creating PG client with connection: ${JSON.stringify(connection)}` - ); + console.debug(`creating PG client with connection: ${JSON.stringify(connection)}`) - const password = await getPassword(connection); + const password = await getPassword(connection) const clientProps: ClientConfig = { host: connection.Host, port: connection.Port, user: connection.Username, password, - database: connection.Database, - }; + database: connection.Database + } - if (connection.SSLMode === "require") { + if (connection.SSLMode === 'require') { clientProps.ssl = { - rejectUnauthorized: false, - }; + rejectUnauthorized: false + } } - let client; - let tries = 0; - let connected = false; + let client + let tries = 0 + let connected = false do { - tries++; - client = new Client(clientProps); + tries++ + client = new Client(clientProps) try { - await client.connect(); + await client.connect() } catch (err) { - console.debug({ err, tries }); - await new Promise((resolve) => setTimeout(resolve, 5000)); - continue; + console.debug({ err, tries }) + await new Promise((resolve) => setTimeout(resolve, 5000)) + continue } - connected = true; - } while (!connected); - console.debug("connected"); - return client; -}; + connected = true + } while (!connected) + console.debug('connected') + return client +} const getPassword = async (connection: Connection) => { const { SecretString } = await secretsmanager.getSecretValue({ - SecretId: connection.PasswordArn, - }); + SecretId: connection.PasswordArn + }) if (!SecretString) { - throw new Error(`cannot find secret with arn ${connection.PasswordArn}`); + throw new Error(`cannot find secret with arn ${connection.PasswordArn}`) } - let parsedSecret; + let parsedSecret try { - parsedSecret = JSON.parse(SecretString); + parsedSecret = JSON.parse(SecretString) } catch (e) { - parsedSecret = SecretString; + parsedSecret = SecretString } - let password; + let password if (isObject(parsedSecret)) { if (!connection.PasswordField) { - throw new Error( - "connection.PasswordField must be specified if secret is object" - ); + throw new Error('connection.PasswordField must be specified if secret is object') } if (!parsedSecret[connection.PasswordField]) { - throw new Error( - `PasswordField ${connection.PasswordField} was not found in secret` - ); + throw new Error(`PasswordField ${connection.PasswordField} was not found in secret`) } - password = parsedSecret[connection.PasswordField]; + password = parsedSecret[connection.PasswordField] } else { - password = parsedSecret; + password = parsedSecret } - return password; -}; + return password +} export const validateConnection = (connection: Connection) => { - if (!("Host" in connection)) { - throw "Connection.Host property is required"; + if (!('Host' in connection)) { + throw 'Connection.Host property is required' } - if (!("Port" in connection)) { - throw "Connection.Port property is required"; + if (!('Port' in connection)) { + throw 'Connection.Port property is required' } - if (!("Database" in connection)) { - throw "Connection.Database property is required"; + if (!('Database' in connection)) { + throw 'Connection.Database property is required' } - if (!("Username" in connection)) { - throw "Connection.Username property is required"; + if (!('Username' in connection)) { + throw 'Connection.Username property is required' } - if (!("PasswordArn" in connection)) { - throw "Connection.PasswordArn property is required"; + if (!('PasswordArn' in connection)) { + throw 'Connection.PasswordArn property is required' } -}; +} export const hashCode = (str: string) => { var hash = 0, i, - chr; + chr for (i = 0; i < str.length; i++) { - chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; // Convert to 32bit integer + chr = str.charCodeAt(i) + hash = (hash << 5) - hash + chr + hash |= 0 // Convert to 32bit integer } - return hash; -}; + return hash +} export function getEnv(name: string): string { - const value = process.env[name]; + const value = process.env[name] if (!value) { - throw new Error(`The environment variable "${name}" is not defined`); + throw new Error(`The environment variable "${name}" is not defined`) } - return value; + return value } export function log(title: any, ...args: any[]) { console.log( - "[cdk-postgresql]", + '[cdk-postgresql]', title, - ...args.map((x) => - typeof x === "object" ? JSON.stringify(x, undefined, 2) : x - ) - ); + ...args.map((x) => (typeof x === 'object' ? JSON.stringify(x, undefined, 2) : x)) + ) } diff --git a/cdk-postgresql/test/constructs.test.ts b/cdk-postgresql/test/constructs.test.ts index 16de81e..a76e1d1 100644 --- a/cdk-postgresql/test/constructs.test.ts +++ b/cdk-postgresql/test/constructs.test.ts @@ -1,187 +1,179 @@ -import { test } from "@jest/globals"; -import { Template } from "aws-cdk-lib/assertions"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import * as cdk from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { Database, Role, Provider } from "../lib"; +import { test } from '@jest/globals' +import { Template } from 'aws-cdk-lib/assertions' +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' +import * as cdk from 'aws-cdk-lib' +import { Construct } from 'constructs' +import { Database, Role, Provider } from '../lib' class TestStack extends cdk.Stack { - readonly exportPrefix: string; + readonly exportPrefix: string constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id); + super(scope, id) } } const getLogicalId = (construct: Construct) => - cdk.Stack.of(construct).getLogicalId( - construct.node.defaultChild as cdk.CfnElement - ); + cdk.Stack.of(construct).getLogicalId(construct.node.defaultChild as cdk.CfnElement) -describe("database", () => { - test("has correct props", () => { - const app = new cdk.App(); - const stack = new TestStack(app, "Stack"); +describe('database', () => { + test('has correct props', () => { + const app = new cdk.App() + const stack = new TestStack(app, 'Stack') - const password = new secretsmanager.Secret(stack, "Password"); + const password = new secretsmanager.Secret(stack, 'Password') - const host = "somedb.com"; - const username = "theusername"; - const name = "mydb"; - const owner = "theowner"; + const host = 'somedb.com' + const username = 'theusername' + const name = 'mydb' + const owner = 'theowner' - const provider = new Provider(stack, "provider", { + const provider = new Provider(stack, 'provider', { host, username, - password, - }); + password + }) - new Database(stack, "DB", { + new Database(stack, 'DB', { name, owner, - provider, - }); + provider + }) - const template = Template.fromStack(stack); - template.resourceCountIs("Custom::Postgresql-Database", 1); - template.hasResourceProperties("Custom::Postgresql-Database", { + const template = Template.fromStack(stack) + template.resourceCountIs('Custom::Postgresql-Database', 1) + template.hasResourceProperties('Custom::Postgresql-Database', { Connection: { Host: host, Port: 5432, - Database: "postgres", + Database: 'postgres', Username: username, PasswordArn: { - Ref: getLogicalId(password), + Ref: getLogicalId(password) }, - SSLMode: "require", + SSLMode: 'require' }, Name: name, - Owner: owner, - }); - }); + Owner: owner + }) + }) - test("creates singleton lambda", () => { - const app = new cdk.App(); - const stack = new TestStack(app, "Stack"); + test('creates singleton lambda', () => { + const app = new cdk.App() + const stack = new TestStack(app, 'Stack') - const password = new secretsmanager.Secret(stack, "Password"); + const password = new secretsmanager.Secret(stack, 'Password') - const host = "somedb.com"; - const username = "theusername"; - const name = "mydb"; - const owner = "theowner"; - const n = 5; + const host = 'somedb.com' + const username = 'theusername' + const name = 'mydb' + const owner = 'theowner' + const n = 5 - const provider = new Provider(stack, "provider", { + const provider = new Provider(stack, 'provider', { host, username, - password, - }); + password + }) for (let i = 0; i < n; i++) { new Database(stack, `DB${i}`, { name, owner, - provider, - }); + provider + }) } - const template = Template.fromStack(stack); + const template = Template.fromStack(stack) // we expect n DBs - template.resourceCountIs("Custom::Postgresql-Database", n); + template.resourceCountIs('Custom::Postgresql-Database', n) // but only 3 Functions: // * 1 for the DB handler (created by us) // * 1 for the DB provider (created by us) // * 1 for the LogRetention (created by the CDK)) - template.resourceCountIs("AWS::Lambda::Function", 3); - }); -}); - -describe("role", () => { - test("has correct props", () => { - const app = new cdk.App(); - const stack = new TestStack(app, "Stack"); - - const connectionPassword = new secretsmanager.Secret( - stack, - "ConnectionPassword" - ); - const rolePassword = new secretsmanager.Secret(stack, "RolePassword"); - - const host = "somedb.com"; - const username = "theusername"; - const name = "rolename"; - const provider = new Provider(stack, "provider", { + template.resourceCountIs('AWS::Lambda::Function', 3) + }) +}) + +describe('role', () => { + test('has correct props', () => { + const app = new cdk.App() + const stack = new TestStack(app, 'Stack') + + const connectionPassword = new secretsmanager.Secret(stack, 'ConnectionPassword') + const rolePassword = new secretsmanager.Secret(stack, 'RolePassword') + + const host = 'somedb.com' + const username = 'theusername' + const name = 'rolename' + const provider = new Provider(stack, 'provider', { host, username, - password: connectionPassword, - }); + password: connectionPassword + }) - new Role(stack, "Role", { + new Role(stack, 'Role', { name, password: rolePassword, - provider, - }); + provider + }) - const template = Template.fromStack(stack); - template.resourceCountIs("Custom::Postgresql-Role", 1); - template.hasResourceProperties("Custom::Postgresql-Role", { + const template = Template.fromStack(stack) + template.resourceCountIs('Custom::Postgresql-Role', 1) + template.hasResourceProperties('Custom::Postgresql-Role', { Connection: { Host: host, Port: 5432, - Database: "postgres", + Database: 'postgres', Username: username, PasswordArn: { - Ref: getLogicalId(connectionPassword), + Ref: getLogicalId(connectionPassword) }, - SSLMode: "require", + SSLMode: 'require' }, Name: name, PasswordArn: { - Ref: getLogicalId(rolePassword), - }, - }); - }); + Ref: getLogicalId(rolePassword) + } + }) + }) - test("creates singleton lambda", () => { - const app = new cdk.App(); - const stack = new TestStack(app, "Stack"); + test('creates singleton lambda', () => { + const app = new cdk.App() + const stack = new TestStack(app, 'Stack') - const connectionPassword = new secretsmanager.Secret( - stack, - "ConnectionPassword" - ); - const rolePassword = new secretsmanager.Secret(stack, "RolePassword"); + const connectionPassword = new secretsmanager.Secret(stack, 'ConnectionPassword') + const rolePassword = new secretsmanager.Secret(stack, 'RolePassword') - const host = "somedb.com"; - const username = "theusername"; - const name = "mydb"; - const n = 5; + const host = 'somedb.com' + const username = 'theusername' + const name = 'mydb' + const n = 5 - const provider = new Provider(stack, "provider", { + const provider = new Provider(stack, 'provider', { host, username, - password: connectionPassword, - }); + password: connectionPassword + }) for (let i = 0; i < n; i++) { new Role(stack, `Role${i}`, { name, password: rolePassword, - provider, - }); + provider + }) } - const template = Template.fromStack(stack); + const template = Template.fromStack(stack) // we expect n Roles - template.resourceCountIs("Custom::Postgresql-Role", n); + template.resourceCountIs('Custom::Postgresql-Role', n) // but only 3 Functions: // * 1 for the Role handler (created by us) // * 1 for the Role provider (created by us) // * 1 for the LogRetention (created by the CDK)) - template.resourceCountIs("AWS::Lambda::Function", 3); - }); -}); + template.resourceCountIs('AWS::Lambda::Function', 3) + }) +}) diff --git a/cdk-postgresql/test/helpers.ts b/cdk-postgresql/test/helpers.ts index 755d045..0ed4884 100644 --- a/cdk-postgresql/test/helpers.ts +++ b/cdk-postgresql/test/helpers.ts @@ -1,70 +1,55 @@ -import { SecretsManager } from "@aws-sdk/client-secrets-manager"; -import { Client } from "pg"; +import { SecretsManager } from '@aws-sdk/client-secrets-manager' +import { Client } from 'pg' const getRoles = async (client: Client) => { - const { rows } = await client.query("SELECT rolname FROM pg_roles"); - return rows.map((r) => r.rolname); -}; + const { rows } = await client.query('SELECT rolname FROM pg_roles') + return rows.map((r) => r.rolname) +} -export const roleExists = async ( - client: Client, - role: string -): Promise => { - const roles = await getRoles(client); - return roles.find((r) => r === role) !== undefined; -}; +export const roleExists = async (client: Client, role: string): Promise => { + const roles = await getRoles(client) + return roles.find((r) => r === role) !== undefined +} const getDatabases = async (client: Client) => { - const { rows } = await client.query( - "SELECT datname FROM pg_database WHERE datistemplate = false" - ); - return rows.map((r) => r.datname); -}; + const { rows } = await client.query('SELECT datname FROM pg_database WHERE datistemplate = false') + return rows.map((r) => r.datname) +} -export const dbExists = async ( - client: Client, - db: string -): Promise => { - const databases = await getDatabases(client); - return databases.find((d) => d === db) !== undefined; -}; +export const dbExists = async (client: Client, db: string): Promise => { + const databases = await getDatabases(client) + return databases.find((d) => d === db) !== undefined +} const makeid = (length: number) => { - var result = ""; - var characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var charactersLength = characters.length; + var result = '' + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + var charactersLength = characters.length for (var i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); + result += characters.charAt(Math.floor(Math.random() * charactersLength)) } - return result; -}; + return result +} -export const createSecret = async ( - secretsManager: SecretsManager, - secretStr: string -): Promise => { +export const createSecret = async (secretsManager: SecretsManager, secretStr: string): Promise => { const response = await secretsManager.createSecret({ SecretString: secretStr, - Name: makeid(10), - }); + Name: makeid(10) + }) if (!response.ARN) { - throw "failed creating secret"; + throw 'failed creating secret' } - return response.ARN; -}; + return response.ARN +} -export const getDbOwner = async ( - client: Client, - db: string -): Promise => { +export const getDbOwner = async (client: Client, db: string): Promise => { const { rows } = await client.query( `SELECT d.datname as dbname, pg_catalog.pg_get_userbyid(d.datdba) as owner FROM pg_catalog.pg_database d WHERE d.datname = '${db}' ORDER BY 1` - ); - const dbRow = rows.find((r) => r.dbname === db); + ) + const dbRow = rows.find((r) => r.dbname === db) if (dbRow === undefined) { - throw new Error(`could not find db with name ${db}`); + throw new Error(`could not find db with name ${db}`) } - return dbRow.owner; -}; + return dbRow.owner +} diff --git a/cdk-postgresql/test/lambda.test.ts b/cdk-postgresql/test/lambda.test.ts index 9696063..5186fea 100644 --- a/cdk-postgresql/test/lambda.test.ts +++ b/cdk-postgresql/test/lambda.test.ts @@ -1,90 +1,90 @@ -import { handler as dbHandler } from "../lib/database.handler"; -import { handler as roleHandler, updateRoleName } from "../lib/role.handler"; -const utilModule = require("../lib/util"); -import { GenericContainer, StartedTestContainer } from "testcontainers"; -import ms from "ms"; +import { handler as dbHandler } from '../lib/database.handler' +import { handler as roleHandler, updateRoleName } from '../lib/role.handler' +const utilModule = require('../lib/util') +import { GenericContainer, StartedTestContainer } from 'testcontainers' +import ms from 'ms' import { CreateDatabaseEvent, CreateRoleEvent, DeleteDatabaseEvent, DeleteRoleEvent, UpdateDatabaseEvent, - UpdateRoleEvent, -} from "../lib/lambda.types"; -import { SecretsManager } from "@aws-sdk/client-secrets-manager"; -import { Client } from "pg"; -import { createDatabase, createRole } from "../lib/postgres"; -import { createSecret, dbExists, getDbOwner, roleExists } from "./helpers"; -import { secretsmanager } from "../lib/util"; - -const DB_PORT = 5432; -const DB_MASTER_USERNAME = "postgres"; -const DB_MASTER_PASSWORD = "masterpwd"; -const DB_DEFAULT_DB = "postgres"; - -let pgContainer: StartedTestContainer; -let localstackContainer: StartedTestContainer; -let masterPasswordArn: string; -let secretsManager: SecretsManager; -let pgHost: string; -let pgPort: number; + UpdateRoleEvent +} from '../lib/lambda.types' +import { SecretsManager } from '@aws-sdk/client-secrets-manager' +import { Client } from 'pg' +import { createDatabase, createRole } from '../lib/postgres' +import { createSecret, dbExists, getDbOwner, roleExists } from './helpers' +import { secretsmanager } from '../lib/util' + +const DB_PORT = 5432 +const DB_MASTER_USERNAME = 'postgres' +const DB_MASTER_PASSWORD = 'masterpwd' +const DB_DEFAULT_DB = 'postgres' + +let pgContainer: StartedTestContainer +let localstackContainer: StartedTestContainer +let masterPasswordArn: string +let secretsManager: SecretsManager +let pgHost: string +let pgPort: number beforeEach(async () => { - pgContainer = await new GenericContainer("postgres") + pgContainer = await new GenericContainer('postgres') .withExposedPorts(DB_PORT) - .withEnv("POSTGRES_PASSWORD", DB_MASTER_PASSWORD) - .start(); - localstackContainer = await new GenericContainer("localstack/localstack") - .withEnv("SERVICES", "secretsmanager") + .withEnv('POSTGRES_PASSWORD', DB_MASTER_PASSWORD) + .start() + localstackContainer = await new GenericContainer('localstack/localstack') + .withEnv('SERVICES', 'secretsmanager') .withExposedPorts(4566) - .start(); + .start() - pgHost = pgContainer.getHost(); - pgPort = pgContainer.getMappedPort(DB_PORT); + pgHost = pgContainer.getHost() + pgPort = pgContainer.getMappedPort(DB_PORT) secretsManager = new SecretsManager({ - endpoint: `http://localhost:${localstackContainer.getMappedPort(4566)}`, - }); - utilModule.secretsmanager = secretsManager; - masterPasswordArn = await createSecret(secretsmanager, DB_MASTER_PASSWORD); -}, ms("2m")); + endpoint: `http://localhost:${localstackContainer.getMappedPort(4566)}` + }) + utilModule.secretsmanager = secretsManager + masterPasswordArn = await createSecret(secretsmanager, DB_MASTER_PASSWORD) +}, ms('2m')) afterEach(async () => { - await pgContainer.stop(); - await localstackContainer.stop(); -}); + await pgContainer.stop() + await localstackContainer.stop() +}) -describe("role", () => { - test("create", async () => { - const newRolePwd = "rolepwd"; - const rolePasswordArn = await createSecret(secretsmanager, newRolePwd); +describe('role', () => { + test('create', async () => { + const newRolePwd = 'rolepwd' + const rolePasswordArn = await createSecret(secretsmanager, newRolePwd) - const newRoleName = "myuser"; + const newRoleName = 'myuser' const event: CreateRoleEvent = { - RequestType: "Create", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - ResourceType: "", + RequestType: 'Create', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newRoleName, - PasswordArn: rolePasswordArn, - }, - }; + PasswordArn: rolePasswordArn + } + } - await roleHandler(event); + await roleHandler(event) // try connecting as the new role const client = new Client({ @@ -92,109 +92,106 @@ describe("role", () => { port: pgPort, database: DB_DEFAULT_DB, user: newRoleName, - password: newRolePwd, - }); - await client.connect(); + password: newRolePwd + }) + await client.connect() - await client.end(); - }); + await client.end() + }) - test("delete", async () => { + test('delete', async () => { const masterClient = new Client({ host: pgHost, port: pgPort, database: DB_DEFAULT_DB, user: DB_MASTER_USERNAME, - password: DB_MASTER_PASSWORD, - }); - await masterClient.connect(); + password: DB_MASTER_PASSWORD + }) + await masterClient.connect() - const newRolePwd = "rolepwd"; - const newRoleName = "myuser"; + const newRolePwd = 'rolepwd' + const newRoleName = 'myuser' await createRole({ client: masterClient, name: newRoleName, - password: newRolePwd, - }); + password: newRolePwd + }) const event: DeleteRoleEvent = { - RequestType: "Delete", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - PhysicalResourceId: "", - ResourceType: "", + RequestType: 'Delete', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + PhysicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newRoleName, - PasswordArn: "", // can be empty for tests - }, - }; + PasswordArn: '' // can be empty for tests + } + } - await roleHandler(event); + await roleHandler(event) - expect(await roleExists(masterClient, newRoleName)).toEqual(false); + expect(await roleExists(masterClient, newRoleName)).toEqual(false) - await masterClient.end(); - }); + await masterClient.end() + }) - test("update", async () => { + test('update', async () => { const masterClient = new Client({ host: pgHost, port: pgPort, database: DB_DEFAULT_DB, user: DB_MASTER_USERNAME, - password: DB_MASTER_PASSWORD, - }); - await masterClient.connect(); + password: DB_MASTER_PASSWORD + }) + await masterClient.connect() - const roleName = "myuser"; - const rolePwd = "rolepwd"; + const roleName = 'myuser' + const rolePwd = 'rolepwd' await createRole({ client: masterClient, name: roleName, - password: rolePwd, - }); + password: rolePwd + }) - const updatedRoleName = roleName + "updated"; - const updatedRolePwd = rolePwd + "updated"; + const updatedRoleName = roleName + 'updated' + const updatedRolePwd = rolePwd + 'updated' - const updatedRolePwdArn = await createSecret( - secretsManager, - updatedRolePwd - ); + const updatedRolePwdArn = await createSecret(secretsManager, updatedRolePwd) const event: UpdateRoleEvent = { - RequestType: "Update", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - PhysicalResourceId: "", - ResourceType: "", + RequestType: 'Update', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + PhysicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: updatedRoleName, - PasswordArn: updatedRolePwdArn, + PasswordArn: updatedRolePwdArn }, OldResourceProperties: { Connection: { @@ -203,14 +200,14 @@ describe("role", () => { Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: roleName, - PasswordArn: "", // can be empty for tests - }, - }; + PasswordArn: '' // can be empty for tests + } + } - await roleHandler(event); + await roleHandler(event) // try connecting as the updated role const client = new Client({ @@ -218,41 +215,41 @@ describe("role", () => { port: pgPort, database: DB_DEFAULT_DB, user: updatedRoleName, - password: updatedRolePwd, - }); - await client.connect(); + password: updatedRolePwd + }) + await client.connect() - await client.end(); - await masterClient.end(); - }); + await client.end() + await masterClient.end() + }) - test("passwordfield", async () => { - const newRolePwd = "rolepwd"; - const masterpassword = "masterpwd"; - const passwordField = "myfield"; + test('passwordfield', async () => { + const newRolePwd = 'rolepwd' + const masterpassword = 'masterpwd' + const passwordField = 'myfield' // the master password is in a secret object const masterPasswordArn = await createSecret( secretsmanager, JSON.stringify({ - [passwordField]: masterpassword, + [passwordField]: masterpassword }) - ); + ) - const rolePasswordArn = await createSecret(secretsmanager, newRolePwd); + const rolePasswordArn = await createSecret(secretsmanager, newRolePwd) - const newRoleName = "myuser"; + const newRoleName = 'myuser' const event: CreateRoleEvent = { - RequestType: "Create", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - ResourceType: "", + RequestType: 'Create', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, @@ -260,14 +257,14 @@ describe("role", () => { Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, PasswordField: passwordField, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newRoleName, - PasswordArn: rolePasswordArn, - }, - }; + PasswordArn: rolePasswordArn + } + } - await roleHandler(event); + await roleHandler(event) // try connecting as the new role const client = new Client({ @@ -275,157 +272,157 @@ describe("role", () => { port: pgPort, database: DB_DEFAULT_DB, user: newRoleName, - password: newRolePwd, - }); - await client.connect(); + password: newRolePwd + }) + await client.connect() - await client.end(); - }); -}); + await client.end() + }) +}) -describe("database", () => { - test("create", async () => { - const newDbName = "mydb"; +describe('database', () => { + test('create', async () => { + const newDbName = 'mydb' const event: CreateDatabaseEvent = { - RequestType: "Create", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - ResourceType: "", + RequestType: 'Create', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newDbName, - Owner: "postgres", - }, - }; - await dbHandler(event); + Owner: 'postgres' + } + } + await dbHandler(event) const client = new Client({ host: pgHost, port: pgPort, database: DB_DEFAULT_DB, user: DB_MASTER_USERNAME, - password: DB_MASTER_PASSWORD, - }); - await client.connect(); + password: DB_MASTER_PASSWORD + }) + await client.connect() - expect(await dbExists(client, newDbName)).toEqual(true); + expect(await dbExists(client, newDbName)).toEqual(true) - await client.end(); - }); + await client.end() + }) - test("delete", async () => { + test('delete', async () => { const masterClient = new Client({ host: pgHost, port: pgPort, database: DB_DEFAULT_DB, user: DB_MASTER_USERNAME, - password: DB_MASTER_PASSWORD, - }); - await masterClient.connect(); + password: DB_MASTER_PASSWORD + }) + await masterClient.connect() - const newDbName = "mydb"; + const newDbName = 'mydb' await createDatabase({ client: masterClient, name: newDbName, - owner: "postgres", - }); + owner: 'postgres' + }) const event: DeleteDatabaseEvent = { - RequestType: "Delete", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - PhysicalResourceId: "", - ResourceType: "", + RequestType: 'Delete', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + PhysicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newDbName, - Owner: "postgres", - }, - }; + Owner: 'postgres' + } + } - await dbHandler(event); + await dbHandler(event) - console.log("checking if db exists"); - expect(await dbExists(masterClient, newDbName)).toEqual(false); - await masterClient.end(); - }); + console.log('checking if db exists') + expect(await dbExists(masterClient, newDbName)).toEqual(false) + await masterClient.end() + }) - test("update db owner", async () => { + test('update db owner', async () => { const masterClient = new Client({ host: pgHost, port: pgPort, database: DB_DEFAULT_DB, user: DB_MASTER_USERNAME, - password: DB_MASTER_PASSWORD, - }); - await masterClient.connect(); + password: DB_MASTER_PASSWORD + }) + await masterClient.connect() - const newDbName = "mydb"; - const newDbRole = "myrole"; - const updatedDbRole = newDbRole + "updated"; + const newDbName = 'mydb' + const newDbRole = 'myrole' + const updatedDbRole = newDbRole + 'updated' await createRole({ client: masterClient, name: newDbRole, - password: "12345", - }); + password: '12345' + }) await createRole({ client: masterClient, name: updatedDbRole, - password: "12345", - }); + password: '12345' + }) await createDatabase({ client: masterClient, name: newDbName, - owner: newDbRole, - }); + owner: newDbRole + }) const event: UpdateDatabaseEvent = { - RequestType: "Update", - ServiceToken: "", - ResponseURL: "", - StackId: "", - RequestId: "", - LogicalResourceId: "", - PhysicalResourceId: "", - ResourceType: "", + RequestType: 'Update', + ServiceToken: '', + ResponseURL: '', + StackId: '', + RequestId: '', + LogicalResourceId: '', + PhysicalResourceId: '', + ResourceType: '', ResourceProperties: { - ServiceToken: "", + ServiceToken: '', Connection: { Host: pgHost, Port: pgPort, Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newDbName, - Owner: updatedDbRole, + Owner: updatedDbRole }, OldResourceProperties: { Connection: { @@ -434,16 +431,16 @@ describe("database", () => { Username: DB_MASTER_USERNAME, Database: DB_DEFAULT_DB, PasswordArn: masterPasswordArn, - SSLMode: "disable", + SSLMode: 'disable' }, Name: newDbName, - Owner: newDbRole, - }, - }; + Owner: newDbRole + } + } - await dbHandler(event); + await dbHandler(event) - expect(await getDbOwner(masterClient, newDbName)).toEqual(updatedDbRole); - await masterClient.end(); - }); -}); + expect(await getDbOwner(masterClient, newDbName)).toEqual(updatedDbRole) + await masterClient.end() + }) +}) diff --git a/cdk-postgresql/test/util.test.ts b/cdk-postgresql/test/util.test.ts index e63d6c9..a71f989 100644 --- a/cdk-postgresql/test/util.test.ts +++ b/cdk-postgresql/test/util.test.ts @@ -1,10 +1,10 @@ -import { isObject } from "../lib/util"; +import { isObject } from '../lib/util' -describe("isObject", () => { - test("obj", () => { - expect(isObject({})).toEqual(true); - expect(isObject({ a: 1 })).toEqual(true); - expect(isObject("something")).toEqual(false); - expect(isObject(null)).toEqual(false); - }); -}); +describe('isObject', () => { + test('obj', () => { + expect(isObject({})).toEqual(true) + expect(isObject({ a: 1 })).toEqual(true) + expect(isObject('something')).toEqual(false) + expect(isObject(null)).toEqual(false) + }) +}) diff --git a/package.json b/package.json index a6d6a68..023bc9a 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,8 @@ "workspaces": [ "cdk-postgresql", "test-stack" - ] + ], + "dependencies": { + "prettier": "^3.8.1" + } } diff --git a/yarn.lock b/yarn.lock index c2fa04e..9ee7451 100644 --- a/yarn.lock +++ b/yarn.lock @@ -954,6 +954,19 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@botpress/cdk-postgresql@2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@botpress/cdk-postgresql/-/cdk-postgresql-2.0.0.tgz#49c804eed7e75a9e2c0056c8a7927c0e8b37750d" + integrity sha512-Mrs0R/AzbJ0xX6DFhN/uPEGTSMirfNVamYy9ODCPnQrt3+KtNx3CDljKp5mfto2h0CtthRgCbb+2s4qmlYyo6Q== + dependencies: + "@aws-sdk/client-secrets-manager" "3.47.2" + "@types/aws-lambda" "^8.10.92" + "@types/node" "17.0.8" + esbuild "^0.14.12" + pg "8.7.1" + pg-format "1.0.4" + verror "^1.10.1" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -4126,6 +4139,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^3.8.1: + version "3.8.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== + pretty-format@^27.0.0, pretty-format@^27.4.6: version "27.4.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" From 87e024ba466856266349f377787317d7550bf3e1 Mon Sep 17 00:00:00 2001 From: allardy Date: Fri, 13 Feb 2026 19:53:31 -0500 Subject: [PATCH 2/4] unsued --- .prettierignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.prettierignore b/.prettierignore index e07a4c8..a8d18d6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,3 @@ -# based on https://gist.github.com/thinkricardo/74f37d82b686de371b0853a5d66d559c - # ignore all files * @@ -17,7 +15,4 @@ pnpm-lock.yaml **/cdk.out/**/* **/*.gomplate.yaml **/README.md -cloud/lib/docker-image/traefik/plugins/htransformation/**/* -# cdktf -**/.gen/** From 640363d62448c8ce5efd652d5ae8b12512614985 Mon Sep 17 00:00:00 2001 From: allardy Date: Fri, 13 Feb 2026 19:54:26 -0500 Subject: [PATCH 3/4] dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 023bc9a..c159e98 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "cdk-postgresql", "test-stack" ], - "dependencies": { + "devDependencies": { "prettier": "^3.8.1" } } From 66d24f2d23fcae5e69ee2f95f85397d1c1820620 Mon Sep 17 00:00:00 2001 From: allardy Date: Mon, 16 Feb 2026 12:53:29 -0500 Subject: [PATCH 4/4] prettier --- test-stack/README.md | 12 +++--- test-stack/bin/myapp.ts | 28 +++++-------- test-stack/cdk.json | 9 +--- test-stack/jest.config.js | 2 +- test-stack/lib/app-stack.ts | 64 ++++++++++++++--------------- test-stack/lib/cluster-stack.ts | 73 +++++++++++++++------------------ 6 files changed, 85 insertions(+), 103 deletions(-) diff --git a/test-stack/README.md b/test-stack/README.md index 3247665..f7d5aef 100644 --- a/test-stack/README.md +++ b/test-stack/README.md @@ -6,9 +6,9 @@ The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands - * `npm run build` compile typescript to js - * `npm run watch` watch for changes and compile - * `npm run test` perform the jest unit tests - * `cdk deploy` deploy this stack to your default AWS account/region - * `cdk diff` compare deployed stack with current state - * `cdk synth` emits the synthesized CloudFormation template +- `npm run build` compile typescript to js +- `npm run watch` watch for changes and compile +- `npm run test` perform the jest unit tests +- `cdk deploy` deploy this stack to your default AWS account/region +- `cdk diff` compare deployed stack with current state +- `cdk synth` emits the synthesized CloudFormation template diff --git a/test-stack/bin/myapp.ts b/test-stack/bin/myapp.ts index 6123805..4701b44 100644 --- a/test-stack/bin/myapp.ts +++ b/test-stack/bin/myapp.ts @@ -1,28 +1,22 @@ #!/usr/bin/env node -import "source-map-support/register"; -import * as cdk from "aws-cdk-lib"; -import { ClusterStack } from "../lib/cluster-stack"; -import { AppStack } from "../lib/app-stack"; +import 'source-map-support/register' +import * as cdk from 'aws-cdk-lib' +import { ClusterStack } from '../lib/cluster-stack' +import { AppStack } from '../lib/app-stack' -const stackName = process.env.STACK_NAME; +const stackName = process.env.STACK_NAME if (!stackName) { - throw new Error("must specify appName"); + throw new Error('must specify appName') } -const app = new cdk.App(); -const { - publicCluster, - privateClusterSecurityGroup, - privateCluster, - vpc, - privateClusterSecret, - publicClusterSecret, -} = new ClusterStack(app, "PGTest"); +const app = new cdk.App() +const { publicCluster, privateClusterSecurityGroup, privateCluster, vpc, privateClusterSecret, publicClusterSecret } = + new ClusterStack(app, 'PGTest') new AppStack(app, stackName, { publicCluster, privateCluster, privateClusterSecurityGroup, publicClusterSecret, vpc, - privateClusterSecret, -}); + privateClusterSecret +}) diff --git a/test-stack/cdk.json b/test-stack/cdk.json index 4f9fcbc..987e145 100644 --- a/test-stack/cdk.json +++ b/test-stack/cdk.json @@ -1,9 +1,7 @@ { "app": "npx ts-node --prefer-ts-exts bin/myapp.ts", "watch": { - "include": [ - "**" - ], + "include": ["**"], "exclude": [ "README.md", "cdk*.json", @@ -22,9 +20,6 @@ "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, "@aws-cdk/aws-lambda:recognizeVersionProps": true, "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, - "@aws-cdk/core:target-partitions": [ - "aws", - "aws-cn" - ] + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] } } diff --git a/test-stack/jest.config.js b/test-stack/jest.config.js index 08263b8..8ab270c 100644 --- a/test-stack/jest.config.js +++ b/test-stack/jest.config.js @@ -5,4 +5,4 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest' } -}; +} diff --git a/test-stack/lib/app-stack.ts b/test-stack/lib/app-stack.ts index 3302ad5..4d2454a 100644 --- a/test-stack/lib/app-stack.ts +++ b/test-stack/lib/app-stack.ts @@ -1,24 +1,24 @@ -import { Provider } from "@botpress/cdk-postgresql/lib/provider"; -import * as cdk from "aws-cdk-lib"; -import * as rds from "aws-cdk-lib/aws-rds"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import * as ec2 from "aws-cdk-lib/aws-ec2"; -import { Construct } from "constructs"; -import { Role } from "@botpress/cdk-postgresql/lib/role"; -import { Database } from "@botpress/cdk-postgresql/lib/database"; +import { Provider } from '@botpress/cdk-postgresql/lib/provider' +import * as cdk from 'aws-cdk-lib' +import * as rds from 'aws-cdk-lib/aws-rds' +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' +import * as ec2 from 'aws-cdk-lib/aws-ec2' +import { Construct } from 'constructs' +import { Role } from '@botpress/cdk-postgresql/lib/role' +import { Database } from '@botpress/cdk-postgresql/lib/database' interface AppStackProps extends cdk.StackProps { - publicCluster: rds.IDatabaseCluster; - privateCluster: rds.IDatabaseCluster; - privateClusterSecurityGroup: ec2.ISecurityGroup; - publicClusterSecret: secretsmanager.ISecret; - privateClusterSecret: secretsmanager.ISecret; - vpc: ec2.IVpc; + publicCluster: rds.IDatabaseCluster + privateCluster: rds.IDatabaseCluster + privateClusterSecurityGroup: ec2.ISecurityGroup + publicClusterSecret: secretsmanager.ISecret + privateClusterSecret: secretsmanager.ISecret + vpc: ec2.IVpc } export class AppStack extends cdk.Stack { constructor(scope: Construct, id: string, props: AppStackProps) { - super(scope, id); + super(scope, id) const { publicCluster, @@ -26,37 +26,37 @@ export class AppStack extends cdk.Stack { privateCluster, privateClusterSecurityGroup, privateClusterSecret, - vpc, - } = props; + vpc + } = props - const rolePassword = new secretsmanager.Secret(this, "RolePassword"); + const rolePassword = new secretsmanager.Secret(this, 'RolePassword') - const privateProvider = new Provider(this, "PrivateProvider", { + const privateProvider = new Provider(this, 'PrivateProvider', { host: privateCluster.clusterEndpoint.hostname, port: privateCluster.clusterEndpoint.port, - username: "postgres", + username: 'postgres', password: privateClusterSecret, - passwordField: "password", + passwordField: 'password', vpc, - securityGroups: [privateClusterSecurityGroup], - }); + securityGroups: [privateClusterSecurityGroup] + }) - const roleName = `role-${this.stackName}`; - const dbName = `db-${this.stackName}`; + const roleName = `role-${this.stackName}` + const dbName = `db-${this.stackName}` - const role = new Role(this, "Role", { + const role = new Role(this, 'Role', { provider: privateProvider, name: roleName, password: rolePassword, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); + removalPolicy: cdk.RemovalPolicy.DESTROY + }) - const db = new Database(this, "DB", { + const db = new Database(this, 'DB', { provider: privateProvider, name: dbName, owner: roleName, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - db.node.addDependency(role); + removalPolicy: cdk.RemovalPolicy.DESTROY + }) + db.node.addDependency(role) } } diff --git a/test-stack/lib/cluster-stack.ts b/test-stack/lib/cluster-stack.ts index f132cc0..2f46626 100644 --- a/test-stack/lib/cluster-stack.ts +++ b/test-stack/lib/cluster-stack.ts @@ -1,67 +1,60 @@ -import { Stack, StackProps } from "aws-cdk-lib"; -import { Construct } from "constructs"; -import * as ec2 from "aws-cdk-lib/aws-ec2"; -import * as rds from "aws-cdk-lib/aws-rds"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; +import { Stack, StackProps } from 'aws-cdk-lib' +import { Construct } from 'constructs' +import * as ec2 from 'aws-cdk-lib/aws-ec2' +import * as rds from 'aws-cdk-lib/aws-rds' +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' export class ClusterStack extends Stack { - public readonly publicCluster: rds.IDatabaseCluster; - public readonly privateCluster: rds.IDatabaseCluster; - public readonly privateClusterSecurityGroup: ec2.ISecurityGroup; - public readonly publicClusterSecret: secretsmanager.ISecret; - public readonly privateClusterSecret: secretsmanager.ISecret; - public readonly vpc: ec2.IVpc; + public readonly publicCluster: rds.IDatabaseCluster + public readonly privateCluster: rds.IDatabaseCluster + public readonly privateClusterSecurityGroup: ec2.ISecurityGroup + public readonly publicClusterSecret: secretsmanager.ISecret + public readonly privateClusterSecret: secretsmanager.ISecret + public readonly vpc: ec2.IVpc constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); + super(scope, id, props) - const vpc = new ec2.Vpc(this, "VPC"); - this.vpc = vpc; + const vpc = new ec2.Vpc(this, 'VPC') + this.vpc = vpc - const publicCluster = new rds.DatabaseCluster(this, "Cluster", { + const publicCluster = new rds.DatabaseCluster(this, 'Cluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ - version: rds.AuroraPostgresEngineVersion.VER_13_4, + version: rds.AuroraPostgresEngineVersion.VER_13_4 }), instanceProps: { vpc, // public so we can reach it over the internet for testing vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }), - publiclyAccessible: true, - }, - }); - publicCluster.connections.allowFromAnyIpv4( - ec2.Port.allTraffic(), - "Open to the world" - ); + publiclyAccessible: true + } + }) + publicCluster.connections.allowFromAnyIpv4(ec2.Port.allTraffic(), 'Open to the world') - const privateClusterSecurityGroup = new ec2.SecurityGroup( - this, - "privateClusterSecurityGroup", - { vpc } - ); - this.privateClusterSecurityGroup = privateClusterSecurityGroup; - const privateCluster = new rds.DatabaseCluster(this, "PrivateCluster", { + const privateClusterSecurityGroup = new ec2.SecurityGroup(this, 'privateClusterSecurityGroup', { vpc }) + this.privateClusterSecurityGroup = privateClusterSecurityGroup + const privateCluster = new rds.DatabaseCluster(this, 'PrivateCluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ - version: rds.AuroraPostgresEngineVersion.VER_13_4, + version: rds.AuroraPostgresEngineVersion.VER_13_4 }), instanceProps: { vpc, - securityGroups: [privateClusterSecurityGroup], - }, - }); - this.publicCluster = publicCluster; - this.privateCluster = privateCluster; + securityGroups: [privateClusterSecurityGroup] + } + }) + this.publicCluster = publicCluster + this.privateCluster = privateCluster if (publicCluster.secret) { - this.publicClusterSecret = publicCluster.secret; + this.publicClusterSecret = publicCluster.secret } else { - throw new Error("public cluster should have secret"); + throw new Error('public cluster should have secret') } if (privateCluster.secret) { - this.privateClusterSecret = privateCluster.secret; + this.privateClusterSecret = privateCluster.secret } else { - throw new Error("private cluster should have secret"); + throw new Error('private cluster should have secret') } } }