Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
18 changes: 18 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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

9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"bracketSpacing": true,
"requirePragma": false,
"arrowParens": "always"
}
64 changes: 32 additions & 32 deletions cdk-postgresql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,56 +13,56 @@
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.

### 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
Expand All @@ -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)
```
10 changes: 5 additions & 5 deletions cdk-postgresql/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/test"],
testMatch: ["**/*.test.ts"],
};
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts']
}
173 changes: 79 additions & 94 deletions cdk-postgresql/lib/database.handler.ts
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading