@mstuercke/pulumi-modules 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ import {type CustomResourceOptions, type Output, Resource} from '@pulumi/pulumi'
2
+ import {RandomPassword} from '@pulumi/random'
3
+ import {DatabaseUser, Project, ProjectIpAccessList, Provider, ServerlessInstance} from '@pulumi/mongodbatlas'
4
+
5
+ export type MongoDBArgs = {
6
+ organizationId: string
7
+ projectId: string
8
+ environmentName: string
9
+ }
10
+
11
+ export class MongoDB extends Resource {
12
+ readonly connectionString: Output<string>
13
+ readonly databaseName: string
14
+ readonly username: string
15
+ readonly password: Output<string>
16
+
17
+ constructor(args: MongoDBArgs, opts?: CustomResourceOptions) {
18
+ const {organizationId, projectId, environmentName} = args
19
+ super('mstuercke:mongodb:MongoDB', organizationId, false, undefined, opts)
20
+
21
+ new Provider('provider', {}, {parent: this})
22
+
23
+ const mongoProject = new Project(
24
+ 'project',
25
+ {
26
+ orgId: organizationId,
27
+ name: `${projectId}-${environmentName}`,
28
+ tags: {
29
+ project: projectId,
30
+ },
31
+ },
32
+ {parent: this},
33
+ )
34
+
35
+ new ProjectIpAccessList(
36
+ 'network-access',
37
+ {
38
+ projectId: mongoProject.id,
39
+ cidrBlock: '0.0.0.0/0',
40
+ },
41
+ {parent: mongoProject},
42
+ )
43
+
44
+ const mongoInstance = new ServerlessInstance(
45
+ 'database',
46
+ {
47
+ projectId: mongoProject.id,
48
+ name: `${projectId}-${environmentName}`,
49
+ providerSettingsBackingProviderName: 'AWS',
50
+ providerSettingsProviderName: 'SERVERLESS',
51
+ providerSettingsRegionName: 'EU_WEST_1', // Ireland
52
+ },
53
+ {parent: mongoProject},
54
+ )
55
+
56
+ const username = `${projectId}-${environmentName}`
57
+ const password = new RandomPassword(
58
+ 'password',
59
+ {
60
+ length: 32,
61
+ },
62
+ {parent: mongoProject},
63
+ )
64
+
65
+ const databaseName = projectId
66
+ new DatabaseUser(
67
+ 'user',
68
+ {
69
+ projectId: mongoProject.id,
70
+ username,
71
+ password: password.result,
72
+ authDatabaseName: 'admin',
73
+ roles: [
74
+ {roleName: 'dbAdmin', databaseName},
75
+ {roleName: 'readWrite', databaseName},
76
+ ],
77
+ },
78
+ {parent: mongoProject},
79
+ )
80
+
81
+ this.connectionString = mongoInstance.connectionStringsStandardSrv
82
+ this.databaseName = databaseName
83
+ this.username = username
84
+ this.password = password.result
85
+ }
86
+ }
@@ -0,0 +1,9 @@
1
+ export * from './MongoDB'
2
+
3
+ import {MongoDB} from './MongoDB'
4
+
5
+ export * from './MongoDB'
6
+
7
+ export const mongodb = {
8
+ MongoDB,
9
+ }
@@ -0,0 +1,226 @@
1
+ import * as aws from '@pulumi/aws'
2
+ import {apigateway, lambda} from '@pulumi/aws'
3
+ import type {CustomResourceOptions, Input} from '@pulumi/pulumi'
4
+ import {Record} from '@pulumi/aws/route53'
5
+ import {BasePathMapping, DomainName} from '@pulumi/aws/apigateway'
6
+ import type {NodeLambdaFunction} from '../lambda'
7
+
8
+ export interface RestApiArgs {
9
+ name: string
10
+ projectId: string
11
+ lambdaFunction: NodeLambdaFunction
12
+ timeoutSeconds?: number
13
+ stageName?: string
14
+ domain: {
15
+ zoneId: Input<string>
16
+ fullName: Input<string>
17
+ usEast1CertificateArn: Input<string>
18
+ }
19
+ }
20
+
21
+ export class RestApi extends aws.apigateway.RestApi {
22
+ readonly url: string
23
+
24
+ constructor(id: string, args: RestApiArgs, opts?: CustomResourceOptions) {
25
+ const {name, projectId, stageName = 'v1', domain} = args
26
+ super(
27
+ id,
28
+ {
29
+ name,
30
+ tags: {
31
+ project: projectId,
32
+ },
33
+ },
34
+ opts,
35
+ )
36
+
37
+ const {lambdaFunction, timeoutSeconds = 29} = args
38
+
39
+ const proxyResource = new apigateway.Resource(
40
+ `proxy`,
41
+ {
42
+ restApi: this.id,
43
+ parentId: this.rootResourceId,
44
+ pathPart: '{proxy+}',
45
+ },
46
+ {parent: this},
47
+ )
48
+
49
+ const proxyMethod = new apigateway.Method(
50
+ `proxy`,
51
+ {
52
+ restApi: this.id,
53
+ resourceId: proxyResource.id,
54
+ httpMethod: 'ANY',
55
+ authorization: 'NONE',
56
+ },
57
+ {parent: proxyResource},
58
+ )
59
+
60
+ const lambdaIntegration = new apigateway.Integration(
61
+ `proxy`,
62
+ {
63
+ restApi: this.id,
64
+ resourceId: proxyResource.id,
65
+ httpMethod: proxyMethod.httpMethod,
66
+ integrationHttpMethod: 'POST',
67
+ type: 'AWS_PROXY',
68
+ uri: lambdaFunction.invokeArn,
69
+ timeoutMilliseconds: timeoutSeconds * 1000,
70
+ },
71
+ {parent: proxyMethod},
72
+ )
73
+
74
+ const proxyOptionsMethod = new apigateway.Method(
75
+ `options`,
76
+ {
77
+ restApi: this.id,
78
+ resourceId: proxyResource.id,
79
+ httpMethod: 'OPTIONS',
80
+ authorization: 'NONE',
81
+ },
82
+ {parent: proxyResource},
83
+ )
84
+
85
+ const lambdaIntegrationOptions = new apigateway.Integration(
86
+ `options`,
87
+ {
88
+ restApi: this.id,
89
+ resourceId: proxyResource.id,
90
+ httpMethod: proxyOptionsMethod.httpMethod,
91
+ integrationHttpMethod: 'POST',
92
+ type: 'AWS_PROXY',
93
+ uri: lambdaFunction.invokeArn,
94
+ timeoutMilliseconds: timeoutSeconds * 1000,
95
+ },
96
+ {parent: proxyOptionsMethod},
97
+ )
98
+
99
+ const proxyRootMethod = new apigateway.Method(
100
+ `proxy-root`,
101
+ {
102
+ restApi: this.id,
103
+ resourceId: this.rootResourceId,
104
+ httpMethod: 'ANY',
105
+ authorization: 'NONE',
106
+ },
107
+ {parent: this},
108
+ )
109
+
110
+ const lambdaRootIntegration = new apigateway.Integration(
111
+ `proxy-root`,
112
+ {
113
+ restApi: this.id,
114
+ resourceId: proxyRootMethod.resourceId,
115
+ httpMethod: proxyRootMethod.httpMethod,
116
+ integrationHttpMethod: 'POST',
117
+ type: 'AWS_PROXY',
118
+ uri: lambdaFunction.invokeArn,
119
+ timeoutMilliseconds: timeoutSeconds * 1000,
120
+ },
121
+ {parent: proxyRootMethod},
122
+ )
123
+
124
+ const proxyRootOptionsMethod = new apigateway.Method(
125
+ `proxy-root-options`,
126
+ {
127
+ restApi: this.id,
128
+ resourceId: this.rootResourceId,
129
+ httpMethod: 'OPTIONS',
130
+ authorization: 'NONE',
131
+ },
132
+ {parent: this},
133
+ )
134
+
135
+ const lambdaRootIntegrationOptions = new apigateway.Integration(
136
+ `proxy-root-options`,
137
+ {
138
+ restApi: this.id,
139
+ resourceId: proxyRootOptionsMethod.resourceId,
140
+ httpMethod: proxyRootOptionsMethod.httpMethod,
141
+ integrationHttpMethod: 'POST',
142
+ type: 'AWS_PROXY',
143
+ uri: lambdaFunction.invokeArn,
144
+ timeoutMilliseconds: timeoutSeconds * 1000,
145
+ },
146
+ {parent: proxyRootOptionsMethod},
147
+ )
148
+
149
+ new lambda.Permission(
150
+ `rest-api`,
151
+ {
152
+ statementId: 'AllowRestApiInvoke',
153
+ action: 'lambda:InvokeFunction',
154
+ function: lambdaFunction.name,
155
+ principal: 'apigateway.amazonaws.com',
156
+ sourceArn: this.executionArn.apply((executionArn) => `${executionArn}/*`),
157
+ },
158
+ {parent: lambdaFunction},
159
+ )
160
+
161
+ const deployment = new apigateway.Deployment(
162
+ stageName,
163
+ {restApi: this.id},
164
+ {
165
+ dependsOn: [
166
+ proxyMethod,
167
+ lambdaIntegration,
168
+ lambdaIntegrationOptions,
169
+ proxyRootMethod,
170
+ lambdaRootIntegration,
171
+ lambdaRootIntegrationOptions,
172
+ ],
173
+ parent: this,
174
+ },
175
+ )
176
+
177
+ const stage = new apigateway.Stage(
178
+ stageName,
179
+ {
180
+ restApi: this.id,
181
+ stageName: stageName,
182
+ deployment: deployment.id,
183
+ },
184
+ {parent: deployment},
185
+ )
186
+
187
+ const domainName = new DomainName(
188
+ 'domain',
189
+ {
190
+ domainName: domain.fullName,
191
+ certificateArn: domain.usEast1CertificateArn,
192
+ },
193
+ {parent: this},
194
+ )
195
+
196
+ new Record(
197
+ 'redirect',
198
+ {
199
+ zoneId: domain.zoneId,
200
+ name: domain.fullName,
201
+ type: 'A',
202
+ aliases: [
203
+ {
204
+ zoneId: domainName.cloudfrontZoneId,
205
+ name: domainName.cloudfrontDomainName,
206
+ evaluateTargetHealth: true,
207
+ },
208
+ ],
209
+ allowOverwrite: true,
210
+ },
211
+ {parent: domainName},
212
+ )
213
+
214
+ new BasePathMapping(
215
+ 'root',
216
+ {
217
+ restApi: this.id,
218
+ stageName: stage.stageName,
219
+ domainName: domainName.domainName,
220
+ },
221
+ {parent: stage},
222
+ )
223
+
224
+ this.url = `https://${domain.fullName}/`
225
+ }
226
+ }
@@ -0,0 +1,7 @@
1
+ import {RestApi} from './RestApi'
2
+
3
+ export * from './RestApi'
4
+
5
+ export const rest = {
6
+ RestApi,
7
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ Bucket,
3
+ BucketAclV2,
4
+ type BucketArgs,
5
+ BucketCorsConfigurationV2,
6
+ BucketOwnershipControls,
7
+ BucketPolicy,
8
+ BucketPublicAccessBlock,
9
+ } from '@pulumi/aws/s3'
10
+ import type {CustomResourceOptions} from '@pulumi/pulumi'
11
+
12
+ type PublicS3BucketArgs = {
13
+ name: string
14
+ allowPresignedPost?: boolean
15
+ tags?: BucketArgs['tags']
16
+ }
17
+
18
+ export class PublicS3Bucket extends Bucket {
19
+ constructor(id: string, args: PublicS3BucketArgs, opts?: CustomResourceOptions) {
20
+ const {name, allowPresignedPost = false, tags} = args
21
+
22
+ super(id, {bucket: name, tags}, opts)
23
+
24
+ const accessBlock = new BucketPublicAccessBlock(
25
+ 'public-access',
26
+ {
27
+ bucket: this.id,
28
+ blockPublicAcls: false,
29
+ blockPublicPolicy: false,
30
+ ignorePublicAcls: false,
31
+ restrictPublicBuckets: false,
32
+ },
33
+ {parent: this},
34
+ )
35
+
36
+ const ownershipControls = new BucketOwnershipControls(
37
+ 'ownership',
38
+ {
39
+ bucket: this.id,
40
+ rule: {objectOwnership: 'BucketOwnerPreferred'},
41
+ },
42
+ {parent: this, dependsOn: [accessBlock]},
43
+ )
44
+
45
+ new BucketAclV2(
46
+ 'acl',
47
+ {
48
+ bucket: this.id,
49
+ acl: 'public-read',
50
+ },
51
+ {parent: this, dependsOn: [ownershipControls]},
52
+ )
53
+
54
+ new BucketPolicy(
55
+ 'policy',
56
+ {
57
+ bucket: this.id,
58
+ policy: this.arn.apply((arn) =>
59
+ JSON.stringify({
60
+ Version: '2012-10-17',
61
+ Statement: [
62
+ {
63
+ Sid: 'Allow Public Access',
64
+ Effect: 'Allow',
65
+ Principal: '*',
66
+ Action: 's3:GetObject',
67
+ Resource: `${arn}/*`,
68
+ },
69
+ ],
70
+ }),
71
+ ),
72
+ },
73
+ {parent: this, dependsOn: [accessBlock]},
74
+ )
75
+
76
+ new BucketCorsConfigurationV2(
77
+ 'allowAll',
78
+ {
79
+ bucket: this.bucket,
80
+ corsRules: [
81
+ {
82
+ maxAgeSeconds: 3000,
83
+ allowedMethods: allowPresignedPost ? ['GET', 'POST'] : ['GET'],
84
+ allowedHeaders: ['*'],
85
+ allowedOrigins: ['*'],
86
+ },
87
+ ],
88
+ },
89
+ {parent: this},
90
+ )
91
+ }
92
+ }
@@ -0,0 +1,7 @@
1
+ import {PublicS3Bucket} from './PublicS3Bucket'
2
+
3
+ export * from './PublicS3Bucket'
4
+
5
+ export const s3 = {
6
+ PublicS3Bucket,
7
+ }
@@ -0,0 +1,159 @@
1
+ import {type CustomResourceOptions, type Input, Resource} from '@pulumi/pulumi'
2
+ import {type PublicS3Bucket, s3} from '../s3'
3
+ import {BucketObject} from '@pulumi/aws/s3'
4
+ import {Distribution} from '@pulumi/aws/cloudfront'
5
+ import {Record} from '@pulumi/aws/route53'
6
+ import {readFilesRecursive} from './readFilesRecursive'
7
+
8
+ export interface StaticWebsiteArgs {
9
+ projectId: string
10
+ environmentName: string
11
+ filesPath: string
12
+ customDomain?: {
13
+ fullName: Input<string>
14
+ zoneId: Input<string>
15
+ usEast1CertificateArn: Input<string>
16
+ }
17
+ }
18
+
19
+ export class StaticWebsite extends Resource {
20
+ public url: string
21
+ public bucket: PublicS3Bucket
22
+
23
+ constructor(id: string, args: StaticWebsiteArgs, opts?: CustomResourceOptions) {
24
+ super('mstuercke:website:StaticWebsite', id, false, undefined, opts)
25
+
26
+ const {projectId, environmentName, filesPath, customDomain} = args
27
+
28
+ const bucket = new s3.PublicS3Bucket(
29
+ 'bucket',
30
+ {
31
+ name: `${projectId}-web-${environmentName}`,
32
+ allowPresignedPost: false,
33
+ tags: {
34
+ project: projectId,
35
+ },
36
+ },
37
+ {parent: this},
38
+ )
39
+
40
+ for (const file of readFilesRecursive(filesPath)) {
41
+ new BucketObject(
42
+ file.relativePath,
43
+ {
44
+ key: file.relativePath,
45
+ bucket: bucket.id,
46
+ source: file.absolutePath,
47
+ contentType: file.mimeType,
48
+ etag: file.md5,
49
+ tags: {
50
+ project: projectId,
51
+ },
52
+ },
53
+ {parent: bucket},
54
+ )
55
+ }
56
+
57
+ const cloudfront = new Distribution(
58
+ `website`,
59
+ {
60
+ enabled: true,
61
+ comment: `${projectId} (${environmentName})`,
62
+ defaultRootObject: 'index.html',
63
+
64
+ origins: [
65
+ {
66
+ domainName: bucket.bucketRegionalDomainName,
67
+ originId: bucket.id,
68
+ },
69
+ ],
70
+
71
+ defaultCacheBehavior: {
72
+ allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
73
+ cachedMethods: ['GET', 'HEAD'],
74
+ targetOriginId: bucket.id,
75
+ viewerProtocolPolicy: 'redirect-to-https',
76
+
77
+ forwardedValues: {
78
+ queryString: false,
79
+ cookies: {
80
+ forward: 'none',
81
+ },
82
+ },
83
+ },
84
+
85
+ orderedCacheBehaviors: [
86
+ {
87
+ pathPattern: 'index.html',
88
+ allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
89
+ cachedMethods: ['GET', 'HEAD'],
90
+ targetOriginId: bucket.id,
91
+ viewerProtocolPolicy: 'redirect-to-https',
92
+ defaultTtl: 0,
93
+ maxTtl: 0,
94
+
95
+ forwardedValues: {
96
+ queryString: false,
97
+ cookies: {
98
+ forward: 'none',
99
+ },
100
+ },
101
+ },
102
+ ],
103
+
104
+ customErrorResponses: [
105
+ {
106
+ errorCode: 404,
107
+ responsePagePath: '/index.html',
108
+ responseCode: 200,
109
+ errorCachingMinTtl: 0,
110
+ },
111
+ ],
112
+
113
+ restrictions: {
114
+ geoRestriction: {
115
+ restrictionType: 'none',
116
+ },
117
+ },
118
+
119
+ viewerCertificate: customDomain
120
+ ? {
121
+ acmCertificateArn: customDomain?.usEast1CertificateArn,
122
+ sslSupportMethod: 'sni-only',
123
+ minimumProtocolVersion: 'TLSv1.2_2019',
124
+ }
125
+ : {cloudfrontDefaultCertificate: true},
126
+
127
+ aliases: customDomain ? [customDomain?.fullName] : undefined,
128
+
129
+ tags: {
130
+ project: projectId,
131
+ },
132
+ },
133
+ {parent: this},
134
+ )
135
+
136
+ if (customDomain) {
137
+ new Record(
138
+ 'redirect',
139
+ {
140
+ zoneId: customDomain?.zoneId,
141
+ name: customDomain?.fullName,
142
+ type: 'A',
143
+ allowOverwrite: true,
144
+ aliases: [
145
+ {
146
+ zoneId: cloudfront.hostedZoneId,
147
+ name: cloudfront.domainName,
148
+ evaluateTargetHealth: false,
149
+ },
150
+ ],
151
+ },
152
+ {parent: this},
153
+ )
154
+ }
155
+
156
+ this.url = `https://${customDomain?.fullName || cloudfront.domainName}`
157
+ this.bucket = bucket
158
+ }
159
+ }
@@ -0,0 +1,7 @@
1
+ import {StaticWebsite} from './StaticWebsite'
2
+
3
+ export * from './StaticWebsite'
4
+
5
+ export const website = {
6
+ StaticWebsite,
7
+ }
@@ -0,0 +1,33 @@
1
+ import {createHash} from 'crypto'
2
+ import * as fs from 'fs'
3
+ import * as mime from 'mime'
4
+ import {globSync} from 'glob'
5
+
6
+ interface File {
7
+ absolutePath: string
8
+ relativePath: string
9
+ md5: string
10
+ mimeType: string
11
+ }
12
+
13
+ export function readFilesRecursive(dir: string): File[] {
14
+ const files = globSync(`${dir}/**/*`, {nodir: true})
15
+
16
+ return files.reduce((previousValue, fullPath) => {
17
+ const content = fs.readFileSync(fullPath, 'base64')
18
+ const md5 = createHash('md5').update(content, 'base64').digest('hex')
19
+
20
+ const mimeType = mime.getType(fullPath)
21
+ if (!mimeType) throw `Cannot determine mimeType for ${fullPath}`
22
+
23
+ return [
24
+ ...previousValue,
25
+ {
26
+ absolutePath: fullPath,
27
+ relativePath: fullPath.replace(`${dir}/`, '').replace(dir, ''),
28
+ md5,
29
+ mimeType,
30
+ },
31
+ ]
32
+ }, [] as File[])
33
+ }