@mstuercke/pulumi-modules 0.0.6 → 0.0.8

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,81 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { Client, ClientCredentials, Connection, ConnectionClient, Provider, Tenant, User } from '@pulumi/auth0';
3
+ export class Auth0 extends Resource {
4
+ domain;
5
+ clientId;
6
+ constructor(id, args, opts) {
7
+ super('mstuercke:auth0:Auth0', id, false, undefined, opts);
8
+ const { projectId, environmentName, tenantDisplayName, clientDisplayName, domain, m2mClientId, allowedUrls, allowedLoginCallbackUrls, allowedLogoutCallbackUrls, nativeAppId, defaultUsers = [], disableSignup = false, } = args;
9
+ new Provider('provider', {}, { parent: this });
10
+ new Tenant('tenant', {
11
+ friendlyName: tenantDisplayName,
12
+ flags: {
13
+ enableClientConnections: false,
14
+ },
15
+ }, { parent: this });
16
+ const nativeAppCallbackUrls = nativeAppId
17
+ ? [
18
+ `${nativeAppId}.auth0://${domain}/ios/${nativeAppId}/callback`,
19
+ `${nativeAppId}.auth0://${domain}/android/${nativeAppId}/callback`,
20
+ ]
21
+ : [];
22
+ const client = new Client('client', {
23
+ name: clientDisplayName,
24
+ appType: 'spa',
25
+ callbacks: [...(allowedLoginCallbackUrls || allowedUrls), ...nativeAppCallbackUrls],
26
+ webOrigins: allowedUrls,
27
+ allowedLogoutUrls: [...(allowedLogoutCallbackUrls || allowedUrls), ...nativeAppCallbackUrls],
28
+ jwtConfiguration: {
29
+ alg: 'RS256',
30
+ },
31
+ addons: {},
32
+ }, { parent: this });
33
+ const connection = new Connection('password-authentication', {
34
+ name: `${projectId}-${environmentName}`,
35
+ strategy: 'auth0',
36
+ options: {
37
+ disableSignup: disableSignup,
38
+ bruteForceProtection: true,
39
+ },
40
+ }, { parent: this });
41
+ const m2mClientConnection = new ConnectionClient('password-authentication-m2m-client', {
42
+ clientId: m2mClientId,
43
+ connectionId: connection.id,
44
+ }, { parent: client });
45
+ const clientConnection = new ConnectionClient('password-authentication-client', {
46
+ clientId: client.id,
47
+ connectionId: connection.id,
48
+ }, { parent: client });
49
+ // TODO: Implement disabling of google-oauth2 (If possible, also disable "Username-Password-Authentication")
50
+ // // disable google-oauth2 on all clients
51
+ // const googleOauth2Connection = output(getConnection({name: 'google-oauth2'}))
52
+ // new ConnectionClients(
53
+ // 'google-oauth2-clien',
54
+ // {
55
+ // connectionId: googleOauth2Connection.id,
56
+ // enabledClients: [],
57
+ // },
58
+ // {parent: client, dependsOn: [client]},
59
+ // )
60
+ new ClientCredentials('credentials', {
61
+ clientId: client.id,
62
+ authenticationMethod: 'none',
63
+ }, { parent: this });
64
+ for (const defaultUser of defaultUsers) {
65
+ const { name, email, emailVerified, password, appMetadata } = defaultUser;
66
+ new User(`default-user-${defaultUser.email}`, {
67
+ connectionName: connection.name,
68
+ name: name,
69
+ email: email,
70
+ emailVerified: emailVerified,
71
+ password: password,
72
+ appMetadata: appMetadata ? JSON.stringify(appMetadata) : undefined,
73
+ }, {
74
+ dependsOn: [m2mClientConnection, clientConnection],
75
+ parent: connection,
76
+ });
77
+ }
78
+ this.domain = domain;
79
+ this.clientId = client.id;
80
+ }
81
+ }
@@ -0,0 +1,5 @@
1
+ import { Auth0 } from './Auth0';
2
+ export * from './Auth0';
3
+ export const auth0 = {
4
+ Auth0,
5
+ };
@@ -0,0 +1,14 @@
1
+ import { acm, route53 } from '@pulumi/aws';
2
+ export const getDomain = async (options) => {
3
+ const { usEast1Provider, name } = options;
4
+ const zone = await route53.getZone({ name });
5
+ const euCentral1Certificate = await acm.getCertificate({ domain: name });
6
+ const usEast1Certificate = await acm.getCertificate({ domain: name }, { provider: usEast1Provider });
7
+ return {
8
+ name: zone.name,
9
+ zoneId: zone.zoneId,
10
+ nameServers: zone.nameServers,
11
+ usEast1CertificateArn: usEast1Certificate.arn,
12
+ euCentral1CertificateArn: euCentral1Certificate.arn,
13
+ };
14
+ };
@@ -0,0 +1,5 @@
1
+ import { getDomain } from './getDomain';
2
+ export * from './getDomain';
3
+ export const domain = {
4
+ getDomain,
5
+ };
@@ -0,0 +1,115 @@
1
+ import { iam } from '@pulumi/aws';
2
+ import { output } from '@pulumi/pulumi';
3
+ export class SimpleIamRolePolicy extends iam.RolePolicy {
4
+ constructor(iamRoleId, config, opts) {
5
+ switch (config.type) {
6
+ case 'custom':
7
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, config.statements), opts);
8
+ break;
9
+ case 'dynamodb':
10
+ if (config.level !== 'full-access')
11
+ throw mapError(config);
12
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
13
+ {
14
+ Action: ['dynamodb:*'],
15
+ Effect: 'Allow',
16
+ Resource: output(config.tableArn).apply((tableArn) => [`${tableArn}`, `${tableArn}/*`]),
17
+ },
18
+ ]), opts);
19
+ break;
20
+ case 'timestream':
21
+ if (config.level !== 'full-access')
22
+ throw mapError(config);
23
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
24
+ {
25
+ Action: ['timestream:*'],
26
+ Effect: 'Allow',
27
+ Resource: [config.dbArn, config.tableArn],
28
+ },
29
+ {
30
+ Action: ['timestream:DescribeEndpoin'],
31
+ Effect: 'Allow',
32
+ Resource: ['*'],
33
+ },
34
+ ]), opts);
35
+ break;
36
+ case 's3':
37
+ if (config.level !== 'full-access')
38
+ throw mapError(config);
39
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
40
+ {
41
+ Action: ['s3:*'],
42
+ Effect: 'Allow',
43
+ Resource: output(config.bucketArn).apply((bucketArn) => [`${bucketArn}`, `${bucketArn}/*`]),
44
+ },
45
+ ]), opts);
46
+ break;
47
+ case 'cognito':
48
+ if (config.level !== 'full-access')
49
+ throw mapError(config);
50
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
51
+ {
52
+ Action: ['cognito-idp:*'],
53
+ Effect: 'Allow',
54
+ Resource: [config.userPoolArn],
55
+ },
56
+ ]), opts);
57
+ break;
58
+ case 'websocket':
59
+ if (config.level !== 'full-access')
60
+ throw mapError(config);
61
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
62
+ {
63
+ Effect: 'Allow',
64
+ Action: 'execute-api:*',
65
+ Resource: config.apiArn,
66
+ },
67
+ ]), opts);
68
+ break;
69
+ case 'sns':
70
+ if (config.level !== 'publish-message')
71
+ throw mapError(config);
72
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
73
+ {
74
+ Effect: 'Allow',
75
+ Action: 'sns:Publish',
76
+ Resource: config.snsTopicArn,
77
+ },
78
+ ]), opts);
79
+ break;
80
+ case 'execute-lambda':
81
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
82
+ {
83
+ Effect: 'Allow',
84
+ Action: 'lambda:InvokeFunction',
85
+ Resource: config.lambdaArn,
86
+ },
87
+ ]), opts);
88
+ break;
89
+ case 'event-bridge':
90
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, [
91
+ {
92
+ Effect: 'Allow',
93
+ Action: ['events:DescribeRule', 'events:DisableRule', 'events:EnableRule'],
94
+ Resource: config.ruleArn,
95
+ },
96
+ ]), opts);
97
+ break;
98
+ default:
99
+ throw mapError(config);
100
+ }
101
+ }
102
+ }
103
+ function mapIamRolePolicyConfig(iamRoleId, name, statements) {
104
+ return {
105
+ role: iamRoleId,
106
+ name: name,
107
+ policy: {
108
+ Version: '2012-10-17',
109
+ Statement: statements,
110
+ },
111
+ };
112
+ }
113
+ function mapError(config) {
114
+ return new Error(`Unable to create policy with config: ${JSON.stringify(config)}`);
115
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { SimpleIamRolePolicy } from './SimpleIamRolePolicy';
2
+ export * from './SimpleIamRolePolicy';
3
+ export * from './SimpleIamRolePolicyConfig';
4
+ export const iam = {
5
+ SimpleIamRolePolicy,
6
+ };
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import { auth0 } from './auth0';
2
+ import { domain } from './domain';
3
+ import { iam } from './iam';
4
+ import { rest } from './rest';
5
+ import { lambda } from './lambda';
6
+ import { mongodb } from './mongodb';
7
+ import { s3 } from './s3';
8
+ import { website } from './website';
9
+ import { websocket } from './websocket';
10
+ export * from './auth0';
11
+ export * from './domain';
12
+ export * from './iam';
13
+ export * from './lambda';
14
+ export * from './mongodb';
15
+ export * from './rest';
16
+ export * from './s3';
17
+ export * from './website';
18
+ export * from './websocket';
19
+ export const mstuercke = {
20
+ auth0,
21
+ domain,
22
+ iam,
23
+ lambda,
24
+ mongodb,
25
+ rest,
26
+ s3,
27
+ website,
28
+ websocket: websocket,
29
+ };
@@ -0,0 +1,71 @@
1
+ import { iam, lambda } from '@pulumi/aws';
2
+ import { getFile } from '@pulumi/archive';
3
+ import { asset, Resource } from '@pulumi/pulumi';
4
+ import { SimpleIamRolePolicy } from '../iam';
5
+ export class NodeLambdaFunction extends Resource {
6
+ name;
7
+ arn;
8
+ invokeArn;
9
+ constructor(args, opts) {
10
+ super('mstuercke:lambda:NodeLambdaFunction', args.name, false, undefined, opts);
11
+ const { name, additionalIamRolePolicies = [], environmentVariables = undefined, runtime = 'nodejs22.x', functionHandler = 'lambdaFunction.lambdaHandler', memorySize = 128, timeoutSeconds = 30, projectId, } = args;
12
+ const role = new iam.Role(name, {
13
+ name: `${name}-lambda`,
14
+ assumeRolePolicy: JSON.stringify({
15
+ Version: '2012-10-17',
16
+ Statement: [
17
+ {
18
+ Action: 'sts:AssumeRole',
19
+ Principal: { Service: 'lambda.amazonaws.com' },
20
+ Effect: 'Allow',
21
+ },
22
+ ],
23
+ }),
24
+ tags: {
25
+ project: projectId,
26
+ },
27
+ }, { parent: this });
28
+ role.id.apply((roleId) => {
29
+ new SimpleIamRolePolicy(roleId, {
30
+ type: 'custom',
31
+ name: 'logging',
32
+ statements: [
33
+ {
34
+ Effect: 'Allow',
35
+ Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEven'],
36
+ Resource: '*',
37
+ },
38
+ ],
39
+ }, { parent: this });
40
+ for (const config of additionalIamRolePolicies) {
41
+ new SimpleIamRolePolicy(roleId, config, { parent: this });
42
+ }
43
+ });
44
+ const lambdaZip = getFile({
45
+ type: 'zip',
46
+ sourceDir: args.lambdaPath,
47
+ outputPath: `dist/lambda/${name}.zip`,
48
+ });
49
+ const lambdaFunction = new lambda.Function(name, {
50
+ name: name,
51
+ handler: functionHandler,
52
+ runtime: runtime,
53
+ memorySize: memorySize,
54
+ timeout: timeoutSeconds,
55
+ code: new asset.FileArchive(lambdaZip.then((lambdaZip) => lambdaZip.outputPath)),
56
+ sourceCodeHash: lambdaZip.then((lambdaZip) => lambdaZip.outputBase64sha256),
57
+ role: role.arn,
58
+ environment: {
59
+ variables: {
60
+ ...environmentVariables,
61
+ },
62
+ },
63
+ tags: {
64
+ project: projectId,
65
+ },
66
+ }, { parent: this });
67
+ this.name = name;
68
+ this.arn = lambdaFunction.arn;
69
+ this.invokeArn = lambdaFunction.invokeArn;
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { NodeLambdaFunction } from './NodeLambdaFunction';
2
+ export * from './NodeLambdaFunction';
3
+ export const lambda = {
4
+ NodeLambdaFunction,
5
+ };
@@ -0,0 +1,51 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { RandomPassword } from '@pulumi/random';
3
+ import { DatabaseUser, Project, ProjectIpAccessList, Provider, ServerlessInstance } from '@pulumi/mongodbatlas';
4
+ export class MongoDB extends Resource {
5
+ connectionString;
6
+ databaseName;
7
+ username;
8
+ password;
9
+ constructor(args, opts) {
10
+ const { organizationId, projectId, environmentName } = args;
11
+ super('mstuercke:mongodb:MongoDB', organizationId, false, undefined, opts);
12
+ new Provider('provider', {}, { parent: this });
13
+ const mongoProject = new Project('project', {
14
+ orgId: organizationId,
15
+ name: `${projectId}-${environmentName}`,
16
+ tags: {
17
+ project: projectId,
18
+ },
19
+ }, { parent: this });
20
+ new ProjectIpAccessList('network-access', {
21
+ projectId: mongoProject.id,
22
+ cidrBlock: '0.0.0.0/0',
23
+ }, { parent: mongoProject });
24
+ const mongoInstance = new ServerlessInstance('database', {
25
+ projectId: mongoProject.id,
26
+ name: `${projectId}-${environmentName}`,
27
+ providerSettingsBackingProviderName: 'AWS',
28
+ providerSettingsProviderName: 'SERVERLESS',
29
+ providerSettingsRegionName: 'EU_WEST_1', // Ireland
30
+ }, { parent: mongoProject });
31
+ const username = `${projectId}-${environmentName}`;
32
+ const password = new RandomPassword('password', {
33
+ length: 32,
34
+ }, { parent: mongoProject });
35
+ const databaseName = projectId;
36
+ new DatabaseUser('user', {
37
+ projectId: mongoProject.id,
38
+ username,
39
+ password: password.result,
40
+ authDatabaseName: 'admin',
41
+ roles: [
42
+ { roleName: 'dbAdmin', databaseName },
43
+ { roleName: 'readWrite', databaseName },
44
+ ],
45
+ }, { parent: mongoProject });
46
+ this.connectionString = mongoInstance.connectionStringsStandardSrv;
47
+ this.databaseName = databaseName;
48
+ this.username = username;
49
+ this.password = password.result;
50
+ }
51
+ }
@@ -0,0 +1,32 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { Project, ProjectIpAccessList, ServerlessInstance } from '@pulumi/mongodbatlas';
3
+ export class MongoDBCustomInstance extends Resource {
4
+ mongoProjectId;
5
+ connectionString;
6
+ databaseName;
7
+ constructor(args, opts) {
8
+ const { organizationId, projectId, environmentName } = args;
9
+ super('mstuercke:mongodb:MongoDBCustomInstance', organizationId, false, undefined, opts);
10
+ const mongoProject = new Project('project', {
11
+ orgId: organizationId,
12
+ name: `${projectId}-${environmentName}`,
13
+ tags: {
14
+ project: projectId,
15
+ },
16
+ }, { parent: this });
17
+ new ProjectIpAccessList('network-access', {
18
+ projectId: mongoProject.id,
19
+ cidrBlock: '0.0.0.0/0',
20
+ }, { parent: this });
21
+ const mongoInstance = new ServerlessInstance('database', {
22
+ projectId: mongoProject.id,
23
+ name: `${projectId}-${environmentName}`,
24
+ providerSettingsBackingProviderName: 'AWS',
25
+ providerSettingsProviderName: 'SERVERLESS',
26
+ providerSettingsRegionName: 'EU_WEST_1', // Ireland
27
+ }, { parent: this });
28
+ this.mongoProjectId = mongoProject.id;
29
+ this.connectionString = mongoInstance.connectionStringsStandardSrv;
30
+ this.databaseName = `${projectId}-${environmentName}`;
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import { output, Resource } from '@pulumi/pulumi';
2
+ import { getProject, getServerlessInstance } from '@pulumi/mongodbatlas';
3
+ export class MongoDBDefaultInstance extends Resource {
4
+ mongoProjectId;
5
+ connectionString;
6
+ databaseName;
7
+ constructor(args, opts) {
8
+ const { organizationId, projectId, environmentName } = args;
9
+ super('mstuercke:mongodb:MongoDBDefaultInstance', organizationId, false, undefined, opts);
10
+ const mongoProject = output(getProject({ name: 'default' }));
11
+ const mongoInstance = mongoProject.id.apply((mongoProjectId) => output(getServerlessInstance({
12
+ projectId: mongoProjectId,
13
+ name: 'default',
14
+ })));
15
+ this.mongoProjectId = mongoProject.id;
16
+ this.connectionString = mongoInstance.connectionStringsStandardSrv;
17
+ this.databaseName = `${projectId}-${environmentName}`;
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { RandomPassword } from '@pulumi/random';
3
+ import { DatabaseUser } from '@pulumi/mongodbatlas';
4
+ export class MongoDBUser extends Resource {
5
+ username;
6
+ password;
7
+ constructor(args, opts) {
8
+ const { mongoProjectId, databaseName, projectId, environmentName } = args;
9
+ const username = `${projectId}-${environmentName}`;
10
+ super('mstuercke:mongodb:MongoDBUser', username, false, undefined, opts);
11
+ const password = new RandomPassword('password', {
12
+ length: 32,
13
+ special: false,
14
+ }, { parent: this });
15
+ new DatabaseUser('user', {
16
+ projectId: mongoProjectId,
17
+ username,
18
+ password: password.result,
19
+ authDatabaseName: 'admin',
20
+ roles: [
21
+ { roleName: 'dbAdmin', databaseName },
22
+ { roleName: 'readWrite', databaseName },
23
+ ],
24
+ }, { parent: this });
25
+ this.username = username;
26
+ this.password = password.result;
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ import { MongoDBCustomInstance } from './MongoDBCustomInstance';
2
+ import { MongoDBDefaultInstance } from './MongoDBDefaultInstance';
3
+ import { MongoDBUser } from './MongoDBUser';
4
+ export * from './MongoDBCustomInstance';
5
+ export * from './MongoDBDefaultInstance';
6
+ export * from './MongoDBUser';
7
+ export const mongodb = {
8
+ MongoDBCustomInstance,
9
+ MongoDBDefaultInstance,
10
+ MongoDBUser,
11
+ };
@@ -0,0 +1,129 @@
1
+ import * as aws from '@pulumi/aws';
2
+ import { apigateway, lambda } from '@pulumi/aws';
3
+ import { Record } from '@pulumi/aws/route53';
4
+ import { BasePathMapping, DomainName } from '@pulumi/aws/apigateway';
5
+ export class RestApi extends aws.apigateway.RestApi {
6
+ url;
7
+ constructor(id, args, opts) {
8
+ const { name, projectId, stageName = 'v1', domain } = args;
9
+ super(id, {
10
+ name,
11
+ tags: {
12
+ project: projectId,
13
+ },
14
+ }, opts);
15
+ const { lambdaFunction, timeoutSeconds = 29 } = args;
16
+ const proxyResource = new apigateway.Resource(`proxy`, {
17
+ restApi: this.id,
18
+ parentId: this.rootResourceId,
19
+ pathPart: '{proxy+}',
20
+ }, { parent: this });
21
+ const proxyMethod = new apigateway.Method(`proxy`, {
22
+ restApi: this.id,
23
+ resourceId: proxyResource.id,
24
+ httpMethod: 'ANY',
25
+ authorization: 'NONE',
26
+ }, { parent: proxyResource });
27
+ const lambdaIntegration = new apigateway.Integration(`proxy`, {
28
+ restApi: this.id,
29
+ resourceId: proxyResource.id,
30
+ httpMethod: proxyMethod.httpMethod,
31
+ integrationHttpMethod: 'POST',
32
+ type: 'AWS_PROXY',
33
+ uri: lambdaFunction.invokeArn,
34
+ timeoutMilliseconds: timeoutSeconds * 1000,
35
+ }, { parent: proxyMethod });
36
+ const proxyOptionsMethod = new apigateway.Method(`options`, {
37
+ restApi: this.id,
38
+ resourceId: proxyResource.id,
39
+ httpMethod: 'OPTIONS',
40
+ authorization: 'NONE',
41
+ }, { parent: proxyResource });
42
+ const lambdaIntegrationOptions = new apigateway.Integration(`options`, {
43
+ restApi: this.id,
44
+ resourceId: proxyResource.id,
45
+ httpMethod: proxyOptionsMethod.httpMethod,
46
+ integrationHttpMethod: 'POST',
47
+ type: 'AWS_PROXY',
48
+ uri: lambdaFunction.invokeArn,
49
+ timeoutMilliseconds: timeoutSeconds * 1000,
50
+ }, { parent: proxyOptionsMethod });
51
+ const proxyRootMethod = new apigateway.Method(`proxy-root`, {
52
+ restApi: this.id,
53
+ resourceId: this.rootResourceId,
54
+ httpMethod: 'ANY',
55
+ authorization: 'NONE',
56
+ }, { parent: this });
57
+ const lambdaRootIntegration = new apigateway.Integration(`proxy-root`, {
58
+ restApi: this.id,
59
+ resourceId: proxyRootMethod.resourceId,
60
+ httpMethod: proxyRootMethod.httpMethod,
61
+ integrationHttpMethod: 'POST',
62
+ type: 'AWS_PROXY',
63
+ uri: lambdaFunction.invokeArn,
64
+ timeoutMilliseconds: timeoutSeconds * 1000,
65
+ }, { parent: proxyRootMethod });
66
+ const proxyRootOptionsMethod = new apigateway.Method(`proxy-root-options`, {
67
+ restApi: this.id,
68
+ resourceId: this.rootResourceId,
69
+ httpMethod: 'OPTIONS',
70
+ authorization: 'NONE',
71
+ }, { parent: this });
72
+ const lambdaRootIntegrationOptions = new apigateway.Integration(`proxy-root-options`, {
73
+ restApi: this.id,
74
+ resourceId: proxyRootOptionsMethod.resourceId,
75
+ httpMethod: proxyRootOptionsMethod.httpMethod,
76
+ integrationHttpMethod: 'POST',
77
+ type: 'AWS_PROXY',
78
+ uri: lambdaFunction.invokeArn,
79
+ timeoutMilliseconds: timeoutSeconds * 1000,
80
+ }, { parent: proxyRootOptionsMethod });
81
+ new lambda.Permission(`rest-api`, {
82
+ statementId: 'AllowRestApiInvoke',
83
+ action: 'lambda:InvokeFunction',
84
+ function: lambdaFunction.name,
85
+ principal: 'apigateway.amazonaws.com',
86
+ sourceArn: this.executionArn.apply((executionArn) => `${executionArn}/*`),
87
+ }, { parent: lambdaFunction });
88
+ const deployment = new apigateway.Deployment(stageName, { restApi: this.id }, {
89
+ dependsOn: [
90
+ proxyMethod,
91
+ lambdaIntegration,
92
+ lambdaIntegrationOptions,
93
+ proxyRootMethod,
94
+ lambdaRootIntegration,
95
+ lambdaRootIntegrationOptions,
96
+ ],
97
+ parent: this,
98
+ });
99
+ const stage = new apigateway.Stage(stageName, {
100
+ restApi: this.id,
101
+ stageName: stageName,
102
+ deployment: deployment.id,
103
+ tags: { project: projectId },
104
+ }, { parent: deployment });
105
+ const domainName = new DomainName('domain', {
106
+ domainName: domain.fullName,
107
+ certificateArn: domain.usEast1CertificateArn,
108
+ }, { parent: this });
109
+ new Record('redirect', {
110
+ zoneId: domain.zoneId,
111
+ name: domain.fullName,
112
+ type: 'A',
113
+ aliases: [
114
+ {
115
+ zoneId: domainName.cloudfrontZoneId,
116
+ name: domainName.cloudfrontDomainName,
117
+ evaluateTargetHealth: true,
118
+ },
119
+ ],
120
+ allowOverwrite: true,
121
+ }, { parent: domainName });
122
+ new BasePathMapping('root', {
123
+ restApi: this.id,
124
+ stageName: stage.stageName,
125
+ domainName: domainName.domainName,
126
+ }, { parent: stage });
127
+ this.url = `https://${domain.fullName}/`;
128
+ }
129
+ }
@@ -0,0 +1,5 @@
1
+ import { RestApi } from './RestApi';
2
+ export * from './RestApi';
3
+ export const rest = {
4
+ RestApi,
5
+ };
@@ -0,0 +1,48 @@
1
+ import { Bucket, BucketAclV2, BucketCorsConfigurationV2, BucketOwnershipControls, BucketPolicy, BucketPublicAccessBlock, } from '@pulumi/aws/s3';
2
+ export class PublicS3Bucket extends Bucket {
3
+ constructor(id, args, opts) {
4
+ const { name, allowPresignedPost = false, tags } = args;
5
+ super(id, { bucket: name, tags }, opts);
6
+ const accessBlock = new BucketPublicAccessBlock('public-access', {
7
+ bucket: this.id,
8
+ blockPublicAcls: false,
9
+ blockPublicPolicy: false,
10
+ ignorePublicAcls: false,
11
+ restrictPublicBuckets: false,
12
+ }, { parent: this });
13
+ const ownershipControls = new BucketOwnershipControls('ownership', {
14
+ bucket: this.id,
15
+ rule: { objectOwnership: 'BucketOwnerPreferred' },
16
+ }, { parent: this, dependsOn: [accessBlock] });
17
+ new BucketAclV2('acl', {
18
+ bucket: this.id,
19
+ acl: 'public-read',
20
+ }, { parent: this, dependsOn: [ownershipControls] });
21
+ new BucketPolicy('policy', {
22
+ bucket: this.id,
23
+ policy: this.arn.apply((arn) => JSON.stringify({
24
+ Version: '2012-10-17',
25
+ Statement: [
26
+ {
27
+ Sid: 'Allow Public Access',
28
+ Effect: 'Allow',
29
+ Principal: '*',
30
+ Action: 's3:GetObject',
31
+ Resource: `${arn}/*`,
32
+ },
33
+ ],
34
+ })),
35
+ }, { parent: this, dependsOn: [accessBlock] });
36
+ new BucketCorsConfigurationV2('allowAll', {
37
+ bucket: this.bucket,
38
+ corsRules: [
39
+ {
40
+ maxAgeSeconds: 3000,
41
+ allowedMethods: allowPresignedPost ? ['GET', 'POST'] : ['GET'],
42
+ allowedHeaders: ['*'],
43
+ allowedOrigins: ['*'],
44
+ },
45
+ ],
46
+ }, { parent: this });
47
+ }
48
+ }
@@ -0,0 +1,5 @@
1
+ import { PublicS3Bucket } from './PublicS3Bucket';
2
+ export * from './PublicS3Bucket';
3
+ export const s3 = {
4
+ PublicS3Bucket,
5
+ };
@@ -0,0 +1,129 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { 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.js';
7
+ export class StaticWebsite extends Resource {
8
+ url;
9
+ bucket;
10
+ constructor(id, args, opts) {
11
+ super('mstuercke:website:StaticWebsite', id, false, undefined, opts);
12
+ const { projectId, environmentName, filesPath, customDomain } = args;
13
+ const bucket = new s3.PublicS3Bucket('bucket', {
14
+ name: `${projectId}-web-${environmentName}`,
15
+ allowPresignedPost: false,
16
+ tags: {
17
+ project: projectId,
18
+ },
19
+ }, { parent: this });
20
+ for (const file of readFilesRecursive(filesPath)) {
21
+ new BucketObject(file.relativePath, {
22
+ key: file.relativePath,
23
+ bucket: bucket.id,
24
+ source: file.absolutePath,
25
+ contentType: file.mimeType,
26
+ etag: file.md5,
27
+ tags: {
28
+ project: projectId,
29
+ },
30
+ }, { parent: bucket });
31
+ }
32
+ const cloudfront = new Distribution(`website`, {
33
+ enabled: true,
34
+ comment: `${projectId} (${environmentName})`,
35
+ defaultRootObject: 'index.html',
36
+ origins: [
37
+ {
38
+ domainName: bucket.bucketRegionalDomainName,
39
+ originId: bucket.id,
40
+ },
41
+ ],
42
+ defaultCacheBehavior: {
43
+ allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
44
+ cachedMethods: ['GET', 'HEAD'],
45
+ targetOriginId: bucket.id,
46
+ viewerProtocolPolicy: 'redirect-to-https',
47
+ forwardedValues: {
48
+ queryString: false,
49
+ cookies: {
50
+ forward: 'none',
51
+ },
52
+ },
53
+ },
54
+ orderedCacheBehaviors: [
55
+ {
56
+ pathPattern: 'index.html',
57
+ allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
58
+ cachedMethods: ['GET', 'HEAD'],
59
+ targetOriginId: bucket.id,
60
+ viewerProtocolPolicy: 'redirect-to-https',
61
+ defaultTtl: 0,
62
+ maxTtl: 0,
63
+ forwardedValues: {
64
+ queryString: false,
65
+ cookies: {
66
+ forward: 'none',
67
+ },
68
+ },
69
+ },
70
+ {
71
+ pathPattern: 'config.json',
72
+ allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
73
+ cachedMethods: ['GET', 'HEAD'],
74
+ targetOriginId: bucket.id,
75
+ viewerProtocolPolicy: 'redirect-to-https',
76
+ defaultTtl: 0,
77
+ maxTtl: 0,
78
+ forwardedValues: {
79
+ queryString: false,
80
+ cookies: {
81
+ forward: 'none',
82
+ },
83
+ },
84
+ },
85
+ ],
86
+ customErrorResponses: [
87
+ {
88
+ errorCode: 404,
89
+ responsePagePath: '/index.html',
90
+ responseCode: 200,
91
+ errorCachingMinTtl: 0,
92
+ },
93
+ ],
94
+ restrictions: {
95
+ geoRestriction: {
96
+ restrictionType: 'none',
97
+ },
98
+ },
99
+ viewerCertificate: customDomain
100
+ ? {
101
+ acmCertificateArn: customDomain?.usEast1CertificateArn,
102
+ sslSupportMethod: 'sni-only',
103
+ minimumProtocolVersion: 'TLSv1.2_2019',
104
+ }
105
+ : { cloudfrontDefaultCertificate: true },
106
+ aliases: customDomain ? [customDomain?.fullName] : undefined,
107
+ tags: {
108
+ project: projectId,
109
+ },
110
+ }, { parent: this });
111
+ if (customDomain) {
112
+ new Record('redirect', {
113
+ zoneId: customDomain?.zoneId,
114
+ name: customDomain?.fullName,
115
+ type: 'A',
116
+ allowOverwrite: true,
117
+ aliases: [
118
+ {
119
+ zoneId: cloudfront.hostedZoneId,
120
+ name: cloudfront.domainName,
121
+ evaluateTargetHealth: false,
122
+ },
123
+ ],
124
+ }, { parent: this });
125
+ }
126
+ this.url = `https://${customDomain?.fullName || cloudfront.domainName}`;
127
+ this.bucket = bucket;
128
+ }
129
+ }
@@ -0,0 +1,5 @@
1
+ import { StaticWebsite } from './StaticWebsite';
2
+ export * from './StaticWebsite';
3
+ export const website = {
4
+ StaticWebsite,
5
+ };
@@ -0,0 +1,23 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs';
3
+ import mime from 'mime';
4
+ import { globSync } from 'glob';
5
+ export function readFilesRecursive(dir) {
6
+ const files = globSync(`${dir}/**/*`, { nodir: true });
7
+ return files.reduce((previousValue, fullPath) => {
8
+ const content = fs.readFileSync(fullPath, 'base64');
9
+ const md5 = createHash('md5').update(content, 'base64').digest('hex');
10
+ const mimeType = mime.getType(fullPath);
11
+ if (!mimeType)
12
+ throw `Cannot determine mimeType for ${fullPath}`;
13
+ return [
14
+ ...previousValue,
15
+ {
16
+ absolutePath: fullPath,
17
+ relativePath: fullPath.replace(`${dir}/`, '').replace(dir, ''),
18
+ md5,
19
+ mimeType,
20
+ },
21
+ ];
22
+ }, []);
23
+ }
@@ -0,0 +1,130 @@
1
+ import { Resource } from '@pulumi/pulumi';
2
+ import { Api, ApiMapping, Deployment, DomainName, Integration, IntegrationResponse, Route, Stage, } from '@pulumi/aws/apigatewayv2';
3
+ import { Policy, Role } from '@pulumi/aws/iam';
4
+ import { Record } from '@pulumi/aws/route53';
5
+ export class WebsocketApi extends Resource {
6
+ id;
7
+ url;
8
+ constructor(id, args, opts) {
9
+ super('mstuercke:websocket:WebsocketApi', id, false, undefined, opts);
10
+ const { name, lambdaFunction, routeThrottling, stageName = 'v1', domain, projectId } = args;
11
+ const api = new Api('api', {
12
+ name,
13
+ protocolType: 'WEBSOCKET',
14
+ routeSelectionExpression: '\\$default',
15
+ tags: {
16
+ project: projectId,
17
+ },
18
+ }, { parent: this });
19
+ const executeLambdaPolicy = new Policy('execute-lambda', {
20
+ name: `${name}-execute-lambda`,
21
+ path: '/',
22
+ policy: {
23
+ Version: '2012-10-17',
24
+ Statement: [
25
+ {
26
+ Action: 'lambda:InvokeFunction',
27
+ Effect: 'Allow',
28
+ Resource: lambdaFunction.arn,
29
+ },
30
+ ],
31
+ },
32
+ tags: {
33
+ project: projectId,
34
+ },
35
+ }, { parent: api });
36
+ const wsApiRole = new Role('role', {
37
+ name: `${name}-role`,
38
+ assumeRolePolicy: JSON.stringify({
39
+ Version: '2012-10-17',
40
+ Statement: [
41
+ {
42
+ Effect: 'Allow',
43
+ Action: 'sts:AssumeRole',
44
+ Principal: { Service: 'apigateway.amazonaws.com' },
45
+ },
46
+ ],
47
+ }),
48
+ tags: {
49
+ project: projectId,
50
+ },
51
+ managedPolicyArns: [executeLambdaPolicy.arn],
52
+ }, { parent: api });
53
+ const wsLambdaIntegration = new Integration('lambda-integration', {
54
+ apiId: api.id,
55
+ integrationType: 'AWS_PROXY',
56
+ integrationUri: lambdaFunction.invokeArn,
57
+ credentialsArn: wsApiRole.arn,
58
+ contentHandlingStrategy: 'CONVERT_TO_TEXT',
59
+ passthroughBehavior: 'WHEN_NO_MATCH',
60
+ }, { parent: this });
61
+ new IntegrationResponse('lambda-integration-response', {
62
+ apiId: api.id,
63
+ integrationId: wsLambdaIntegration.id,
64
+ integrationResponseKey: '/200/',
65
+ }, { parent: wsLambdaIntegration });
66
+ const routes = [];
67
+ const routeKeys = ['$connect', '$default', '$disconnect'];
68
+ for (const routeKey of routeKeys) {
69
+ const route = new Route(`${routeKey}-route`, {
70
+ apiId: api.id,
71
+ routeKey: routeKey,
72
+ target: wsLambdaIntegration.id.apply((id) => `integrations/${id}`),
73
+ authorizationType: 'NONE',
74
+ }, { parent: wsLambdaIntegration });
75
+ routes.push(route);
76
+ }
77
+ const deployment = new Deployment('deployment', {
78
+ apiId: api.id,
79
+ }, {
80
+ dependsOn: [wsLambdaIntegration, ...routes],
81
+ parent: api,
82
+ });
83
+ const stage = new Stage('stage', {
84
+ apiId: api.id,
85
+ name: stageName,
86
+ deploymentId: deployment.id,
87
+ defaultRouteSettings: {
88
+ throttlingRateLimit: routeThrottling?.rateLimit ?? 100,
89
+ throttlingBurstLimit: routeThrottling?.burstLimit ?? 100,
90
+ },
91
+ tags: {
92
+ project: projectId,
93
+ },
94
+ }, { parent: deployment });
95
+ const domainName = new DomainName('domain', {
96
+ domainName: domain.fullName,
97
+ domainNameConfiguration: {
98
+ certificateArn: domain.euCentral1CertificateArn,
99
+ endpointType: 'REGIONAL',
100
+ securityPolicy: 'TLS_1_2',
101
+ },
102
+ }, { parent: api });
103
+ new Record('redirect', {
104
+ zoneId: domain.zoneId,
105
+ name: domain.fullName,
106
+ type: 'A',
107
+ aliases: [
108
+ {
109
+ zoneId: domainName.domainNameConfiguration.hostedZoneId,
110
+ name: domainName.domainNameConfiguration.targetDomainName,
111
+ evaluateTargetHealth: true,
112
+ },
113
+ ],
114
+ allowOverwrite: true,
115
+ }, { parent: api });
116
+ new ApiMapping('root', {
117
+ apiId: api.id,
118
+ domainName: domainName.id,
119
+ stage: stage.name,
120
+ }, { parent: api });
121
+ new ApiMapping('stage', {
122
+ apiId: api.id,
123
+ domainName: domainName.id,
124
+ stage: stage.name,
125
+ apiMappingKey: stage.name,
126
+ }, { parent: api });
127
+ this.id = api.id;
128
+ this.url = `wss://${domain.fullName}/`;
129
+ }
130
+ }
@@ -0,0 +1,5 @@
1
+ import { WebsocketApi } from './WebsocketApi';
2
+ export * from './WebsocketApi';
3
+ export const websocket = {
4
+ WebsocketApi,
5
+ };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@mstuercke/pulumi-modules",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "files": [
5
- "dist/**/!(*.test|*.fixture).ts",
5
+ "dist/**/!(*.test|*.fixture){.d.ts,.js}",
6
6
  "README.md"
7
7
  ],
8
8
  "exports": "./dist/index.js",