-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathdatabase-query.ts
135 lines (120 loc) · 4.91 KB
/
database-query.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import * as path from 'path';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as cdk from 'aws-cdk-lib/core';
import * as customresources from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { DatabaseQueryHandlerProps } from './handler-props';
import { Cluster } from '../cluster';
import { DatabaseOptions } from '../database-options';
export interface DatabaseQueryProps<HandlerProps> extends DatabaseOptions {
readonly handler: string;
readonly properties: HandlerProps;
/**
* The policy to apply when this resource is removed from the application.
*
* @default cdk.RemovalPolicy.Destroy
*/
readonly removalPolicy?: cdk.RemovalPolicy;
/**
* The handler timeout duration
*
* @default cdk.Duration.minutes(1)
*/
readonly timeout?: cdk.Duration;
}
export class DatabaseQuery<HandlerProps> extends Construct implements iam.IGrantable {
readonly grantPrincipal: iam.IPrincipal;
readonly ref: string;
private readonly resource: cdk.CustomResource;
constructor(scope: Construct, id: string, props: DatabaseQueryProps<HandlerProps>) {
super(scope, id);
if (props.timeout && !cdk.Token.isUnresolved(props.timeout)) {
if (props.timeout.toMilliseconds() < cdk.Duration.seconds(1).toMilliseconds()) {
throw new Error(`The timeout for the handler must be BETWEEN 1 second and 15 minutes, got ${props.timeout.toMilliseconds()} milliseconds.`);
}
if (props.timeout.toSeconds() > cdk.Duration.minutes(15).toSeconds()) {
throw new Error(`The timeout for the handler must be between 1 second and 15 minutes, got ${props.timeout.toSeconds()} seconds.`);
}
}
const adminUser = this.getAdminUser(props);
const handler = new lambda.SingletonFunction(this, 'Handler', {
code: lambda.Code.fromAsset(path.join(__dirname, 'database-query-provider'), {
exclude: ['*.ts'],
}),
runtime: lambda.determineLatestNodeRuntime(this),
handler: 'index.handler',
timeout: props.timeout ?? cdk.Duration.minutes(1),
uuid: '3de5bea7-27da-4796-8662-5efb56431b5f',
lambdaPurpose: 'Query Redshift Database',
});
handler.addToRolePolicy(new iam.PolicyStatement({
actions: ['redshift-data:DescribeStatement', 'redshift-data:ExecuteStatement'],
resources: ['*'],
}));
adminUser.grantRead(handler);
const provider = new customresources.Provider(this, 'Provider', {
onEventHandler: handler,
role: this.getOrCreateInvokerRole(handler),
});
const queryHandlerProps: DatabaseQueryHandlerProps & HandlerProps = {
handler: props.handler,
clusterName: props.cluster.clusterName,
adminUserArn: adminUser.secretArn,
databaseName: props.databaseName,
...props.properties,
};
this.resource = new cdk.CustomResource(this, 'Resource', {
resourceType: 'Custom::RedshiftDatabaseQuery',
serviceToken: provider.serviceToken,
removalPolicy: props.removalPolicy,
properties: queryHandlerProps,
});
this.grantPrincipal = handler.grantPrincipal;
this.ref = this.resource.ref;
}
public applyRemovalPolicy(policy: cdk.RemovalPolicy): void {
this.resource.applyRemovalPolicy(policy);
}
public getAtt(attributeName: string): cdk.Reference {
return this.resource.getAtt(attributeName);
}
public getAttString(attributeName: string): string {
return this.resource.getAttString(attributeName);
}
private getAdminUser(props: DatabaseOptions): secretsmanager.ISecret {
const cluster = props.cluster;
let adminUser = props.adminUser;
if (!adminUser) {
if (cluster instanceof Cluster) {
if (cluster.secret) {
adminUser = cluster.secret;
} else {
throw new Error(
'Administrative access to the Redshift cluster is required but an admin user secret was not provided and the cluster did not generate admin user credentials (they were provided explicitly)',
);
}
} else {
throw new Error(
'Administrative access to the Redshift cluster is required but an admin user secret was not provided and the cluster was imported',
);
}
}
return adminUser;
}
/**
* Get or create the IAM role for the singleton lambda function.
* We only need one function since it's just acting as an invoker.
* */
private getOrCreateInvokerRole(handler: lambda.SingletonFunction): iam.IRole {
const id = handler.constructName + 'InvokerRole';
const existing = cdk.Stack.of(this).node.tryFindChild(id);
return existing != null
? existing as iam.Role
: new iam.Role(cdk.Stack.of(this), id, {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
});
}
}