diff --git a/packages/pulumi-aws/src/apps/admin/AdminApp.ts b/packages/pulumi-aws/src/apps/admin/AdminApp.ts index 4b54f95088..41b8ce4e06 100644 --- a/packages/pulumi-aws/src/apps/admin/AdminApp.ts +++ b/packages/pulumi-aws/src/apps/admin/AdminApp.ts @@ -97,8 +97,11 @@ export function createAdminApp(config?: AdminAppConfig & ApplicationConfig) } }, async app(ctx) { + // Create the app instance. const app = new ApiApp(ctx); + // Run the default application setup. await app.setup(config || {}); + // Run the custom user config. await config?.config?.(app, ctx); return app; }, diff --git a/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts b/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts index b420970c66..498c498a11 100644 --- a/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts +++ b/packages/pulumi-aws/src/apps/api/ApiApwScheduler.ts @@ -1,7 +1,7 @@ import path from "path"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; interface ScheduleActionParams { env: Record; @@ -14,47 +14,52 @@ const EXECUTE_ACTION_LAMBDA = `${LAMBDA_NAME_PREFIX}-execute-action-lambda`; const EVENT_RULE_NAME = `${LAMBDA_NAME_PREFIX}-event-rule`; const EVENT_RULE_TARGET = `${LAMBDA_NAME_PREFIX}-event-rule-target`; -export function createApwScheduler(app: PulumiApp, params: ScheduleActionParams) { - const executeAction = createExecuteActionLambda(app, params); - const scheduleAction = createScheduleActionLambda(app, executeAction.lambda.output, params); +export type ApiApwScheduler = PulumiAppModule; - // Create event rule. - const eventRule = app.addResource(aws.cloudwatch.EventRule, { - name: EVENT_RULE_NAME, - config: { - description: `Enable us to schedule an action in publishing workflow at a particular datetime`, - scheduleExpression: "cron(* * * * ? 2000)", - isEnabled: true - } - }); +export const ApiApwScheduler = defineAppModule({ + name: "ApiApwScheduler", + config(app: PulumiApp, params: ScheduleActionParams) { + const executeAction = createExecuteActionLambda(app, params); + const scheduleAction = createScheduleActionLambda(app, executeAction.lambda.output, params); - // Add required permission to the target lambda. - app.addResource(aws.lambda.Permission, { - name: "eventTargetPermission", - config: { - action: "lambda:InvokeFunction", - function: scheduleAction.lambda.output.arn, - principal: "events.amazonaws.com", - statementId: "allow-rule-invoke-" + EVENT_RULE_NAME - } - }); + // Create event rule. + const eventRule = app.addResource(aws.cloudwatch.EventRule, { + name: EVENT_RULE_NAME, + config: { + description: `Enable us to schedule an action in publishing workflow at a particular datetime`, + scheduleExpression: "cron(* * * * ? 2000)", + isEnabled: true + } + }); - // Add lambda as target to the event rule. - const eventTarget = app.addResource(aws.cloudwatch.EventTarget, { - name: EVENT_RULE_TARGET, - config: { - rule: eventRule.output.name, - arn: scheduleAction.lambda.output.arn - } - }); + // Add required permission to the target lambda. + app.addResource(aws.lambda.Permission, { + name: "eventTargetPermission", + config: { + action: "lambda:InvokeFunction", + function: scheduleAction.lambda.output.arn, + principal: "events.amazonaws.com", + statementId: "allow-rule-invoke-" + EVENT_RULE_NAME + } + }); - return { - executeAction, - scheduleAction, - eventRule, - eventTarget - }; -} + // Add lambda as target to the event rule. + const eventTarget = app.addResource(aws.cloudwatch.EventTarget, { + name: EVENT_RULE_TARGET, + config: { + rule: eventRule.output.name, + arn: scheduleAction.lambda.output.arn + } + }); + + return { + executeAction, + scheduleAction, + eventRule, + eventTarget + }; + } +}); function createExecuteActionLambda(app: PulumiApp, params: ScheduleActionParams) { const role = app.addResource(aws.iam.Role, { diff --git a/packages/pulumi-aws/src/apps/api/ApiCloudfront.ts b/packages/pulumi-aws/src/apps/api/ApiCloudfront.ts index c621c1020a..72637ea9b1 100644 --- a/packages/pulumi-aws/src/apps/api/ApiCloudfront.ts +++ b/packages/pulumi-aws/src/apps/api/ApiCloudfront.ts @@ -1,54 +1,21 @@ import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; import { ApiGateway } from "./ApiGateway"; -export interface ApiCloudfrontParams { - apiGateway: ApiGateway; -} +export type ApiCloudfront = PulumiAppModule; -export function createCloudfront(app: PulumiApp, params: ApiCloudfrontParams) { - return app.addResource(aws.cloudfront.Distribution, { - name: "api-cloudfront", - config: { - waitForDeployment: false, - defaultCacheBehavior: { - compress: true, - allowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"], - cachedMethods: ["GET", "HEAD", "OPTIONS"], - forwardedValues: { - cookies: { - forward: "none" - }, - headers: ["Accept", "Accept-Language"], - queryString: true - }, - // MinTTL <= DefaultTTL <= MaxTTL - minTtl: 0, - defaultTtl: 0, - maxTtl: 86400, - targetOriginId: params.apiGateway.api.output.name, - viewerProtocolPolicy: "allow-all" - }, - isIpv6Enabled: true, - enabled: true, - orderedCacheBehaviors: [ - { +export const ApiCloudfront = defineAppModule({ + name: "ApiCloudfront", + config(app: PulumiApp) { + const gateway = app.getModule(ApiGateway); + + return app.addResource(aws.cloudfront.Distribution, { + name: "api-cloudfront", + config: { + waitForDeployment: false, + defaultCacheBehavior: { compress: true, - allowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"], - cachedMethods: ["GET", "HEAD", "OPTIONS"], - forwardedValues: { - cookies: { - forward: "none" - }, - headers: ["Accept", "Accept-Language"], - queryString: true - }, - pathPattern: "/cms*", - viewerProtocolPolicy: "allow-all", - targetOriginId: params.apiGateway.api.output.name - }, - { allowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"], cachedMethods: ["GET", "HEAD", "OPTIONS"], forwardedValues: { @@ -61,37 +28,89 @@ export function createCloudfront(app: PulumiApp, params: ApiCloudfrontParams) { // MinTTL <= DefaultTTL <= MaxTTL minTtl: 0, defaultTtl: 0, - maxTtl: 2592000, - pathPattern: "/files/*", - viewerProtocolPolicy: "allow-all", - targetOriginId: params.apiGateway.api.output.name - } - ], - origins: [ - { - domainName: params.apiGateway.defaultStage.output.invokeUrl.apply( - (url: string) => new URL(url).hostname - ), - originPath: params.apiGateway.defaultStage.output.invokeUrl.apply( - (url: string) => new URL(url).pathname - ), - originId: params.apiGateway.api.output.name, - customOriginConfig: { - httpPort: 80, - httpsPort: 443, - originProtocolPolicy: "https-only", - originSslProtocols: ["TLSv1.2"] + maxTtl: 86400, + targetOriginId: gateway.api.output.name, + viewerProtocolPolicy: "allow-all" + }, + isIpv6Enabled: true, + enabled: true, + orderedCacheBehaviors: [ + { + compress: true, + allowedMethods: [ + "GET", + "HEAD", + "OPTIONS", + "PUT", + "POST", + "PATCH", + "DELETE" + ], + cachedMethods: ["GET", "HEAD", "OPTIONS"], + forwardedValues: { + cookies: { + forward: "none" + }, + headers: ["Accept", "Accept-Language"], + queryString: true + }, + pathPattern: "/cms*", + viewerProtocolPolicy: "allow-all", + targetOriginId: gateway.api.output.name + }, + { + allowedMethods: [ + "GET", + "HEAD", + "OPTIONS", + "PUT", + "POST", + "PATCH", + "DELETE" + ], + cachedMethods: ["GET", "HEAD", "OPTIONS"], + forwardedValues: { + cookies: { + forward: "none" + }, + headers: ["Accept", "Accept-Language"], + queryString: true + }, + // MinTTL <= DefaultTTL <= MaxTTL + minTtl: 0, + defaultTtl: 0, + maxTtl: 2592000, + pathPattern: "/files/*", + viewerProtocolPolicy: "allow-all", + targetOriginId: gateway.api.output.name } + ], + origins: [ + { + domainName: gateway.stage.output.invokeUrl.apply( + (url: string) => new URL(url).hostname + ), + originPath: gateway.stage.output.invokeUrl.apply( + (url: string) => new URL(url).pathname + ), + originId: gateway.api.output.name, + customOriginConfig: { + httpPort: 80, + httpsPort: 443, + originProtocolPolicy: "https-only", + originSslProtocols: ["TLSv1.2"] + } + } + ], + restrictions: { + geoRestriction: { + restrictionType: "none" + } + }, + viewerCertificate: { + cloudfrontDefaultCertificate: true } - ], - restrictions: { - geoRestriction: { - restrictionType: "none" - } - }, - viewerCertificate: { - cloudfrontDefaultCertificate: true } - } - }); -} + }); + } +}); diff --git a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts index ff5fcb5599..e9188fdb24 100644 --- a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts +++ b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts @@ -4,7 +4,7 @@ import * as aws from "@pulumi/aws"; // @ts-ignore import { getLayerArn } from "@webiny/aws-layers"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; import { Vpc } from "./ApiVpc"; import { createLambdaRole } from "./ApiLambdaUtils"; @@ -14,136 +14,141 @@ interface FileManagerParams { vpc: Vpc | undefined; } -export function createFileManager(app: PulumiApp, params: FileManagerParams) { - const policy = createFileManagerLambdaPolicy(app, params); - const role = createLambdaRole(app, { - name: "fm-lambda-role", - policy: policy.output, - vpc: params.vpc - }); +export type ApiFileManager = PulumiAppModule; - const transform = app.addResource(aws.lambda.Function, { - name: "fm-image-transformer", - config: { - handler: "handler.handler", - timeout: 30, - runtime: "nodejs14.x", - memorySize: 1600, - role: role.output.arn, - description: "Performs image optimization, resizing, etc.", - code: new pulumi.asset.AssetArchive({ - ".": new pulumi.asset.FileArchive( - path.join(app.ctx.appDir, "code/fileManager/transform/build") - ) - }), - layers: [getLayerArn("sharp")], - environment: { - variables: { S3_BUCKET: params.fileManagerBucketId } - }, - vpcConfig: params.vpc - ? { - subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), - securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] - } - : undefined - } - }); +export const ApiFileManager = defineAppModule({ + name: "ApiFileManager", + config(app: PulumiApp, params: FileManagerParams) { + const policy = createFileManagerLambdaPolicy(app, params); + const role = createLambdaRole(app, { + name: "fm-lambda-role", + policy: policy.output, + vpc: params.vpc + }); - const manage = app.addResource(aws.lambda.Function, { - name: "fm-manage", - config: { - role: role.output.arn, - runtime: "nodejs14.x", - handler: "handler.handler", - timeout: 30, - memorySize: 512, - description: "Triggered when a file is deleted.", - code: new pulumi.asset.AssetArchive({ - ".": new pulumi.asset.FileArchive( - path.join(app.ctx.appDir, "code/fileManager/manage/build") - ) - }), - environment: { - variables: { S3_BUCKET: params.fileManagerBucketId } - }, - vpcConfig: params.vpc - ? { - subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), - securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] - } - : undefined - } - }); + const transform = app.addResource(aws.lambda.Function, { + name: "fm-image-transformer", + config: { + handler: "handler.handler", + timeout: 30, + runtime: "nodejs14.x", + memorySize: 1600, + role: role.output.arn, + description: "Performs image optimization, resizing, etc.", + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive( + path.join(app.ctx.appDir, "code/fileManager/transform/build") + ) + }), + layers: [getLayerArn("sharp")], + environment: { + variables: { S3_BUCKET: params.fileManagerBucketId } + }, + vpcConfig: params.vpc + ? { + subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), + securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] + } + : undefined + } + }); - const download = app.addResource(aws.lambda.Function, { - name: "fm-download", - config: { - role: role.output.arn, - runtime: "nodejs14.x", - handler: "handler.handler", - timeout: 30, - memorySize: 512, - description: "Serves previously uploaded files.", - code: new pulumi.asset.AssetArchive({ - ".": new pulumi.asset.FileArchive( - path.join(app.ctx.appDir, "code/fileManager/download/build") - ) - }), - environment: { - variables: { - S3_BUCKET: params.fileManagerBucketId, - IMAGE_TRANSFORMER_FUNCTION: transform.output.arn - } - }, - vpcConfig: params.vpc - ? { - subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), - securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] - } - : undefined - } - }); + const manage = app.addResource(aws.lambda.Function, { + name: "fm-manage", + config: { + role: role.output.arn, + runtime: "nodejs14.x", + handler: "handler.handler", + timeout: 30, + memorySize: 512, + description: "Triggered when a file is deleted.", + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive( + path.join(app.ctx.appDir, "code/fileManager/manage/build") + ) + }), + environment: { + variables: { S3_BUCKET: params.fileManagerBucketId } + }, + vpcConfig: params.vpc + ? { + subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), + securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] + } + : undefined + } + }); - const manageS3LambdaPermission = app.addResource(aws.lambda.Permission, { - name: "fm-manage-s3-lambda-permission", - config: { - action: "lambda:InvokeFunction", - function: manage.output.arn, - principal: "s3.amazonaws.com", - sourceArn: pulumi.interpolate`arn:aws:s3:::${params.fileManagerBucketId}` - }, - opts: { - dependsOn: [manage.output] - } - }); + const download = app.addResource(aws.lambda.Function, { + name: "fm-download", + config: { + role: role.output.arn, + runtime: "nodejs14.x", + handler: "handler.handler", + timeout: 30, + memorySize: 512, + description: "Serves previously uploaded files.", + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive( + path.join(app.ctx.appDir, "code/fileManager/download/build") + ) + }), + environment: { + variables: { + S3_BUCKET: params.fileManagerBucketId, + IMAGE_TRANSFORMER_FUNCTION: transform.output.arn + } + }, + vpcConfig: params.vpc + ? { + subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), + securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] + } + : undefined + } + }); - const bucketNotification = app.addResource(aws.s3.BucketNotification, { - name: "bucketNotification", - config: { - bucket: params.fileManagerBucketId, - lambdaFunctions: [ - { - lambdaFunctionArn: manage.output.arn, - events: ["s3:ObjectRemoved:*"] - } - ] - }, - opts: { - dependsOn: [manage.output, manageS3LambdaPermission.output] - } - }); + const manageS3LambdaPermission = app.addResource(aws.lambda.Permission, { + name: "fm-manage-s3-lambda-permission", + config: { + action: "lambda:InvokeFunction", + function: manage.output.arn, + principal: "s3.amazonaws.com", + sourceArn: pulumi.interpolate`arn:aws:s3:::${params.fileManagerBucketId}` + }, + opts: { + dependsOn: [manage.output] + } + }); + + const bucketNotification = app.addResource(aws.s3.BucketNotification, { + name: "bucketNotification", + config: { + bucket: params.fileManagerBucketId, + lambdaFunctions: [ + { + lambdaFunctionArn: manage.output.arn, + events: ["s3:ObjectRemoved:*"] + } + ] + }, + opts: { + dependsOn: [manage.output, manageS3LambdaPermission.output] + } + }); - const functions = { - transform, - manage, - download - }; + const functions = { + transform, + manage, + download + }; - return { - functions, - bucketNotification - }; -} + return { + functions, + bucketNotification + }; + } +}); function createFileManagerLambdaPolicy(app: PulumiApp, params: FileManagerParams) { return app.addResource(aws.iam.Policy, { diff --git a/packages/pulumi-aws/src/apps/api/ApiGateway.ts b/packages/pulumi-aws/src/apps/api/ApiGateway.ts index 5318c0d690..acf02ddb30 100644 --- a/packages/pulumi-aws/src/apps/api/ApiGateway.ts +++ b/packages/pulumi-aws/src/apps/api/ApiGateway.ts @@ -1,6 +1,6 @@ import * as aws from "@pulumi/aws"; import * as pulumi from "@pulumi/pulumi"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; export interface ApiRouteParams { path: pulumi.Input; @@ -8,43 +8,46 @@ export interface ApiRouteParams { function: pulumi.Input; } -export type ApiGateway = ReturnType; +export type ApiGateway = PulumiAppModule; -export function createApiGateway(app: PulumiApp, routes: Record) { - const api = app.addResource(aws.apigatewayv2.Api, { - name: "api-gateway", - config: { - protocolType: "HTTP", - description: "Main API gateway" - } - }); +export const ApiGateway = defineAppModule({ + name: "ApiGateway", + config(app: PulumiApp, routesConfig: Record) { + const api = app.addResource(aws.apigatewayv2.Api, { + name: "api-gateway", + config: { + protocolType: "HTTP", + description: "Main API gateway" + } + }); - const defaultStage = app.addResource(aws.apigatewayv2.Stage, { - name: "default", - config: { - apiId: api.output.id, - autoDeploy: true - } - }); + const stage = app.addResource(aws.apigatewayv2.Stage, { + name: "default", + config: { + apiId: api.output.id, + autoDeploy: true + } + }); - const routesResult: Record> = {}; + const routes: Record> = {}; - for (const name of Object.keys(routes)) { - addRoute(name, routes[name]); - } + for (const name of Object.keys(routesConfig)) { + addRoute(name, routesConfig[name]); + } - return { - api, - defaultStage, - routes: routesResult, - addRoute - }; + return { + api, + stage, + routes, + addRoute + }; - function addRoute(name: string, params: ApiRouteParams) { - const route = createRoute(app, api.output, name, params); - routesResult[name] = route; + function addRoute(name: string, params: ApiRouteParams) { + const route = createRoute(app, api.output, name, params); + routes[name] = route; + } } -} +}); function createRoute( app: PulumiApp, diff --git a/packages/pulumi-aws/src/apps/api/ApiGraphql.ts b/packages/pulumi-aws/src/apps/api/ApiGraphql.ts index 27e99ab835..2d7ca06509 100644 --- a/packages/pulumi-aws/src/apps/api/ApiGraphql.ts +++ b/packages/pulumi-aws/src/apps/api/ApiGraphql.ts @@ -2,7 +2,7 @@ import path from "path"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; import { Vpc } from "./ApiVpc"; import { createLambdaRole } from "./ApiLambdaUtils"; @@ -21,71 +21,80 @@ interface GraphqlParams { vpc: Vpc | undefined; } -export function createGraphql(app: PulumiApp, params: GraphqlParams) { - const policy = createGraphqlLambdaPolicy(app, params); - const role = createLambdaRole(app, { - name: "api-lambda-role", - policy: policy.output, - vpc: params.vpc - }); +export type ApiGraphql = PulumiAppModule; - const graphql = app.addResource(aws.lambda.Function, { - name: "graphql", - config: { - runtime: "nodejs14.x", - handler: "handler.handler", - role: role.output.arn, - timeout: 30, - memorySize: 512, - code: new pulumi.asset.AssetArchive({ - ".": new pulumi.asset.FileArchive(path.join(app.ctx.appDir, "code/graphql/build")) - }), - environment: { - variables: { - ...params.env, - AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", - WCP_ENVIRONMENT_API_KEY: String(process.env["WCP_ENVIRONMENT_API_KEY"]) - } - }, - vpcConfig: params.vpc - ? { - subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), - securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] - } - : undefined - } - }); +export const ApiGraphql = defineAppModule({ + name: "ApiGraphql", + config(app: PulumiApp, params: GraphqlParams) { + const policy = createGraphqlLambdaPolicy(app, params); + const role = createLambdaRole(app, { + name: "api-lambda-role", + policy: policy.output, + vpc: params.vpc + }); - /** - * Store meta information like "mainGraphqlFunctionArn" in APW settings at deploy time. - * - * Note: We can't pass "mainGraphqlFunctionArn" as env variable due to circular dependency between - * "graphql" lambda and "api-apw-scheduler-execute-action" lambda. - */ - app.addResource(aws.dynamodb.TableItem, { - name: "apwSettings", - config: { - tableName: params.primaryDynamodbTableName, - hashKey: params.primaryDynamodbTableHashKey, - rangeKey: pulumi.output(params.primaryDynamodbTableRangeKey).apply(key => key || "SK"), - item: pulumi.interpolate`{ + const graphql = app.addResource(aws.lambda.Function, { + name: "graphql", + config: { + runtime: "nodejs14.x", + handler: "handler.handler", + role: role.output.arn, + timeout: 30, + memorySize: 512, + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive( + path.join(app.ctx.appDir, "code/graphql/build") + ) + }), + environment: { + variables: { + ...params.env, + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + WCP_ENVIRONMENT_API_KEY: String(process.env["WCP_ENVIRONMENT_API_KEY"]) + } + }, + vpcConfig: params.vpc + ? { + subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), + securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] + } + : undefined + } + }); + + /** + * Store meta information like "mainGraphqlFunctionArn" in APW settings at deploy time. + * + * Note: We can't pass "mainGraphqlFunctionArn" as env variable due to circular dependency between + * "graphql" lambda and "api-apw-scheduler-execute-action" lambda. + */ + app.addResource(aws.dynamodb.TableItem, { + name: "apwSettings", + config: { + tableName: params.primaryDynamodbTableName, + hashKey: params.primaryDynamodbTableHashKey, + rangeKey: pulumi + .output(params.primaryDynamodbTableRangeKey) + .apply(key => key || "SK"), + item: pulumi.interpolate`{ "PK": {"S": "APW#SETTINGS"}, "SK": {"S": "A"}, "mainGraphqlFunctionArn": {"S": "${graphql.output.arn}"}, "eventRuleName": {"S": "${params.apwSchedulerEventRule.name}"}, "eventTargetId": {"S": "${params.apwSchedulerEventTarget.targetId}"} }` - } - }); + } + }); - return { - role, - policy, - functions: { - graphql - } - }; -} + return { + role, + policy, + functions: { + graphql + } + }; + } +}); function createGraphqlLambdaPolicy(app: PulumiApp, params: GraphqlParams) { return app.addResource(aws.iam.Policy, { diff --git a/packages/pulumi-aws/src/apps/api/ApiHeadlessCMS.ts b/packages/pulumi-aws/src/apps/api/ApiHeadlessCMS.ts index 047a3fa3a5..1ff4c99211 100644 --- a/packages/pulumi-aws/src/apps/api/ApiHeadlessCMS.ts +++ b/packages/pulumi-aws/src/apps/api/ApiHeadlessCMS.ts @@ -2,7 +2,7 @@ import path from "path"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; import { Vpc } from "./ApiVpc"; import { createLambdaRole } from "./ApiLambdaUtils"; @@ -13,50 +13,55 @@ interface HeadlessCMSParams { vpc: Vpc | undefined; } -export function createHeadlessCms(app: PulumiApp, params: HeadlessCMSParams) { - const policy = createHeadlessCmsLambdaPolicy(app, params); - const role = createLambdaRole(app, { - name: "headless-cms-lambda-role", - policy: policy.output, - vpc: params.vpc - }); +export type ApiHeadlessCMS = PulumiAppModule; - const graphql = app.addResource(aws.lambda.Function, { - name: "headless-cms", - config: { - runtime: "nodejs14.x", - handler: "handler.handler", - role: role.output.arn, - timeout: 30, - memorySize: 512, - code: new pulumi.asset.AssetArchive({ - ".": new pulumi.asset.FileArchive( - path.join(app.ctx.appDir, "code/headlessCMS/build") - ) - }), - environment: { - variables: { - ...params.env, - AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" - } - }, - vpcConfig: params.vpc - ? { - subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), - securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] - } - : undefined - } - }); +export const ApiHeadlessCMS = defineAppModule({ + name: "ApiHeadlessCMS", + config(app: PulumiApp, params: HeadlessCMSParams) { + const policy = createHeadlessCmsLambdaPolicy(app, params); + const role = createLambdaRole(app, { + name: "headless-cms-lambda-role", + policy: policy.output, + vpc: params.vpc + }); - return { - role, - policy, - functions: { - graphql - } - }; -} + const graphql = app.addResource(aws.lambda.Function, { + name: "headless-cms", + config: { + runtime: "nodejs14.x", + handler: "handler.handler", + role: role.output.arn, + timeout: 30, + memorySize: 512, + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive( + path.join(app.ctx.appDir, "code/headlessCMS/build") + ) + }), + environment: { + variables: { + ...params.env, + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" + } + }, + vpcConfig: params.vpc + ? { + subnetIds: params.vpc.subnets.private.map(subNet => subNet.output.id), + securityGroupIds: [params.vpc.vpc.output.defaultSecurityGroupId] + } + : undefined + } + }); + + return { + role, + policy, + functions: { + graphql + } + }; + } +}); function createHeadlessCmsLambdaPolicy(app: PulumiApp, params: HeadlessCMSParams) { return app.addResource(aws.iam.Policy, { diff --git a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts index 738a92ef52..adf4f394f4 100644 --- a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts +++ b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts @@ -4,7 +4,7 @@ import * as aws from "@pulumi/aws"; //@ts-ignore import { createInstallationZip } from "@webiny/api-page-builder/installation"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; import { Vpc } from "./ApiVpc"; import { createLambdaRole } from "./ApiLambdaUtils"; @@ -18,31 +18,36 @@ interface PageBuilderParams { vpc: Vpc | undefined; } -export function createPageBuilder(app: PulumiApp, params: PageBuilderParams) { - app.addHandler(() => { - const pbInstallationZipPath = path.join(path.resolve(), ".tmp", "pbInstallation.zip"); - // Will create "pbInstallation.zip" and save it in the `pbInstallationZipPath` path. - createInstallationZip(pbInstallationZipPath); +export type ApiPageBuilder = PulumiAppModule; - new aws.s3.BucketObject("./pbInstallation.zip", { - key: "pbInstallation.zip", - acl: "public-read", - bucket: params.fileManagerBucketId, - contentType: "application/octet-stream", - source: new pulumi.asset.FileAsset(pbInstallationZipPath) +export const ApiPageBuilder = defineAppModule({ + name: "ApiPageBuilder", + config(app: PulumiApp, params: PageBuilderParams) { + app.addHandler(() => { + const pbInstallationZipPath = path.join(path.resolve(), ".tmp", "pbInstallation.zip"); + // Will create "pbInstallation.zip" and save it in the `pbInstallationZipPath` path. + createInstallationZip(pbInstallationZipPath); + + new aws.s3.BucketObject("./pbInstallation.zip", { + key: "pbInstallation.zip", + acl: "public-read", + bucket: params.fileManagerBucketId, + contentType: "application/octet-stream", + source: new pulumi.asset.FileAsset(pbInstallationZipPath) + }); }); - }); - const updateSettings = createUpdateSettingsResources(app, params); - const exportPages = createExportPagesResources(app, params); - const importPages = createImportPagesResources(app, params); + const updateSettings = createUpdateSettingsResources(app, params); + const exportPages = createExportPagesResources(app, params); + const importPages = createImportPagesResources(app, params); - return { - updateSettings, - exportPages, - importPages - }; -} + return { + updateSettings, + exportPages, + importPages + }; + } +}); function createUpdateSettingsResources(app: PulumiApp, params: PageBuilderParams) { const policy = createUpdateSettingsLambdaPolicy(app, params); diff --git a/packages/pulumi-aws/src/apps/api/index.ts b/packages/pulumi-aws/src/apps/api/index.ts new file mode 100644 index 0000000000..bd6dbb8a4f --- /dev/null +++ b/packages/pulumi-aws/src/apps/api/index.ts @@ -0,0 +1,8 @@ +export * from "./ApiApp"; +export * from "./ApiApwScheduler"; +export * from "./ApiCloudfront"; +export * from "./ApiFileManager"; +export * from "./ApiGateway"; +export * from "./ApiGraphql"; +export * from "./ApiHeadlessCMS"; +export * from "./ApiPageBuilder"; diff --git a/packages/pulumi-aws/src/apps/index.ts b/packages/pulumi-aws/src/apps/index.ts index 75c2f1aefa..889d37c860 100644 --- a/packages/pulumi-aws/src/apps/index.ts +++ b/packages/pulumi-aws/src/apps/index.ts @@ -1,5 +1,5 @@ -export * from "./storage/StorageApp"; -export * from "./api/ApiApp"; +export * from "./storage"; +export * from "./api"; export * from "./admin/AdminApp"; export * from "./website/WebsiteApp"; export { CustomDomainParams } from "./customDomain"; diff --git a/packages/pulumi-aws/src/apps/storage/StorageApp.ts b/packages/pulumi-aws/src/apps/storage/StorageApp.ts index 313de0fbb2..1885990a7f 100644 --- a/packages/pulumi-aws/src/apps/storage/StorageApp.ts +++ b/packages/pulumi-aws/src/apps/storage/StorageApp.ts @@ -6,9 +6,9 @@ import { ApplicationConfig } from "@webiny/pulumi-sdk"; -import { createCognitoResources } from "./StorageCognito"; -import { createDynamoTable } from "./StorageDynamo"; -import { createFileManagerBucket } from "./StorageFileManager"; +import { StorageCognito } from "./StorageCognito"; +import { StorageDynamo } from "./StorageDynamo"; +import { StorageFileManger } from "./StorageFileManager"; export interface StorageAppConfig extends Partial { protect?(ctx: ApplicationContext): boolean; @@ -26,16 +26,16 @@ export const StorageApp = defineApp({ const legacyConfig = config?.legacy?.(app.ctx) ?? {}; // Setup DynamoDB table - const dynamoDbTable = createDynamoTable(app, { protect }); + const dynamoDbTable = app.addModule(StorageDynamo, { protect }); // Setup Cognito - const cognito = createCognitoResources(app, { + const cognito = app.addModule(StorageCognito, { protect, useEmailAsUsername: legacyConfig.useEmailAsUsername ?? false }); // Setup file storage bucket - const fileManagerBucket = createFileManagerBucket(app, { protect }); + const fileManagerBucket = app.addModule(StorageFileManger, { protect }); app.addOutputs({ fileManagerBucketId: fileManagerBucket.output.id, @@ -65,10 +65,12 @@ export function createStorageApp(config?: StorageAppConfig & ApplicationConfig; + +export const StorageCognito = defineAppModule({ + name: "Cognito", + config(app: PulumiApp, params: StorageCognitoParams) { + const userPool = app.addResource(aws.cognito.UserPool, { + name: "user-pool", + config: { + passwordPolicy: { + minimumLength: 8, + requireLowercase: false, + requireNumbers: false, + requireSymbols: false, + requireUppercase: false, + temporaryPasswordValidityDays: 7 }, - { - attributeDataType: "String", - name: "family_name", - required: true, - developerOnlyAttribute: false, - mutable: true, - stringAttributeConstraints: { - maxLength: "2048", - minLength: "0" - } + adminCreateUserConfig: { + allowAdminCreateUserOnly: true + }, + autoVerifiedAttributes: ["email"], + emailConfiguration: { + emailSendingAccount: "COGNITO_DEFAULT" + }, + // In a legacy setup we use email as username. + // We need to provide a way for users to have this setup, + // because changing it would require whole cognito pool to be recreated. + usernameAttributes: params.useEmailAsUsername ? ["email"] : undefined, + aliasAttributes: params.useEmailAsUsername ? undefined : ["preferred_username"], + lambdaConfig: {}, + mfaConfiguration: "OFF", + userPoolAddOns: { + advancedSecurityMode: "OFF" /* required */ }, - { - attributeDataType: "String", - name: "given_name", - required: true, - developerOnlyAttribute: false, - mutable: true, - stringAttributeConstraints: { - maxLength: "2048", - minLength: "0" + verificationMessageTemplate: { + defaultEmailOption: "CONFIRM_WITH_CODE" + }, + schemas: [ + { + attributeDataType: "String", + name: "email", + required: true, + developerOnlyAttribute: false, + mutable: true, + stringAttributeConstraints: { + maxLength: "2048", + minLength: "0" + } + }, + { + attributeDataType: "String", + name: "family_name", + required: true, + developerOnlyAttribute: false, + mutable: true, + stringAttributeConstraints: { + maxLength: "2048", + minLength: "0" + } + }, + { + attributeDataType: "String", + name: "given_name", + required: true, + developerOnlyAttribute: false, + mutable: true, + stringAttributeConstraints: { + maxLength: "2048", + minLength: "0" + } } - } - ] - }, - opts: { - protect: params.protect - } - }); + ] + }, + opts: { + protect: params.protect + } + }); - const userPoolClient = app.addResource(aws.cognito.UserPoolClient, { - name: "user-pool-client", - config: { - userPoolId: userPool.output.id - } - }); + const userPoolClient = app.addResource(aws.cognito.UserPoolClient, { + name: "user-pool-client", + config: { + userPoolId: userPool.output.id + } + }); - return { - userPool, - userPoolClient - }; -} + return { + userPool, + userPoolClient + }; + } +}); diff --git a/packages/pulumi-aws/src/apps/storage/StorageDynamo.ts b/packages/pulumi-aws/src/apps/storage/StorageDynamo.ts index 51f2695d96..9ab92c708e 100644 --- a/packages/pulumi-aws/src/apps/storage/StorageDynamo.ts +++ b/packages/pulumi-aws/src/apps/storage/StorageDynamo.ts @@ -1,30 +1,35 @@ import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; -export function createDynamoTable(app: PulumiApp, params: { protect: boolean }) { - return app.addResource(aws.dynamodb.Table, { - name: "webiny", - config: { - attributes: [ - { name: "PK", type: "S" }, - { name: "SK", type: "S" }, - { name: "GSI1_PK", type: "S" }, - { name: "GSI1_SK", type: "S" } - ], - billingMode: "PAY_PER_REQUEST", - hashKey: "PK", - rangeKey: "SK", - globalSecondaryIndexes: [ - { - name: "GSI1", - hashKey: "GSI1_PK", - rangeKey: "GSI1_SK", - projectionType: "ALL" - } - ] - }, - opts: { - protect: params.protect - } - }); -} +export type StorageDynamo = PulumiAppModule; + +export const StorageDynamo = defineAppModule({ + name: "DynamoDb", + config(app: PulumiApp, params: { protect: boolean }) { + return app.addResource(aws.dynamodb.Table, { + name: "webiny", + config: { + attributes: [ + { name: "PK", type: "S" }, + { name: "SK", type: "S" }, + { name: "GSI1_PK", type: "S" }, + { name: "GSI1_SK", type: "S" } + ], + billingMode: "PAY_PER_REQUEST", + hashKey: "PK", + rangeKey: "SK", + globalSecondaryIndexes: [ + { + name: "GSI1", + hashKey: "GSI1_PK", + rangeKey: "GSI1_SK", + projectionType: "ALL" + } + ] + }, + opts: { + protect: params.protect + } + }); + } +}); diff --git a/packages/pulumi-aws/src/apps/storage/StorageFileManager.ts b/packages/pulumi-aws/src/apps/storage/StorageFileManager.ts index 8ee3b1a241..d6a7d3a621 100644 --- a/packages/pulumi-aws/src/apps/storage/StorageFileManager.ts +++ b/packages/pulumi-aws/src/apps/storage/StorageFileManager.ts @@ -1,24 +1,28 @@ import * as aws from "@pulumi/aws"; -import { PulumiApp } from "@webiny/pulumi-sdk"; +import { defineAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi-sdk"; -export function createFileManagerBucket(app: PulumiApp, params: { protect: boolean }) { - return app.addResource(aws.s3.Bucket, { - name: "fm-bucket", - config: { - acl: "private", - // We definitely don't want to force-destroy if "protected" flag is true. - forceDestroy: !params.protect, - corsRules: [ - { - allowedHeaders: ["*"], - allowedMethods: ["POST", "GET"], - allowedOrigins: ["*"], - maxAgeSeconds: 3000 - } - ] - }, - opts: { - protect: params.protect - } - }); -} +export type StorageFileManger = PulumiAppModule; +export const StorageFileManger = defineAppModule({ + name: "FileManagerBucket", + config(app: PulumiApp, params: { protect: boolean }) { + return app.addResource(aws.s3.Bucket, { + name: "fm-bucket", + config: { + acl: "private", + // We definitely don't want to force-destroy if "protected" flag is true. + forceDestroy: !params.protect, + corsRules: [ + { + allowedHeaders: ["*"], + allowedMethods: ["POST", "GET"], + allowedOrigins: ["*"], + maxAgeSeconds: 3000 + } + ] + }, + opts: { + protect: params.protect + } + }); + } +}); diff --git a/packages/pulumi-aws/src/apps/storage/index.ts b/packages/pulumi-aws/src/apps/storage/index.ts new file mode 100644 index 0000000000..0bb6468e6f --- /dev/null +++ b/packages/pulumi-aws/src/apps/storage/index.ts @@ -0,0 +1,4 @@ +export * from "./StorageApp"; +export * from "./StorageCognito"; +export * from "./StorageDynamo"; +export * from "./StorageFileManager"; diff --git a/packages/pulumi-aws/src/apps/website/WebsiteApp.ts b/packages/pulumi-aws/src/apps/website/WebsiteApp.ts index d5b0992a2f..d0171f3884 100644 --- a/packages/pulumi-aws/src/apps/website/WebsiteApp.ts +++ b/packages/pulumi-aws/src/apps/website/WebsiteApp.ts @@ -173,8 +173,11 @@ export function createWebsiteApp(config?: WebsiteAppConfig & ApplicationConfig { def: PulumiAppModuleDefinition, config?: TConfig ) { + if (this.modules.has(def.symbol)) { + throw new Error( + `Module "${def.name}" is already present in the "${this.name}" application.` + ); + } + const module = def.run(this, config as TConfig); this.modules.set(def.symbol, module); @@ -179,31 +186,6 @@ export abstract class PulumiApp { } } -export interface PulumiAppModuleCallback { - (this: void, app: PulumiApp, config: TConfig): TModule; -} - -export interface PulumiAppModuleParams { - name: string; - config: PulumiAppModuleCallback; -} - -export class PulumiAppModuleDefinition { - public readonly symbol = Symbol(); - public readonly name: string; - public readonly run: PulumiAppModuleCallback; - constructor(params: PulumiAppModuleParams) { - this.name = params.name; - this.run = params.config; - } -} - -export function defineAppModule( - params: PulumiAppModuleParams -) { - return new PulumiAppModuleDefinition(params); -} - export interface CreateAppParams, TConfig = void> { name: string; config(app: PulumiApp, config: TConfig): TOutput | Promise; diff --git a/packages/pulumi-sdk/src/PulumiAppModule.ts b/packages/pulumi-sdk/src/PulumiAppModule.ts new file mode 100644 index 0000000000..b26fbf2097 --- /dev/null +++ b/packages/pulumi-sdk/src/PulumiAppModule.ts @@ -0,0 +1,31 @@ +// There is a circular dependency between the two. +// This trick allow us to make it work. +type PulumiApp = import("./PulumiApp").PulumiApp; + +export interface PulumiAppModuleCallback { + (this: void, app: PulumiApp, config: TConfig): TModule; +} + +export interface PulumiAppModuleParams { + name: string; + config: PulumiAppModuleCallback; +} + +export type PulumiAppModule> = + T extends PulumiAppModuleDefinition ? V : never; + +export class PulumiAppModuleDefinition { + public readonly symbol = Symbol(); + public readonly name: string; + public readonly run: PulumiAppModuleCallback; + constructor(params: PulumiAppModuleParams) { + this.name = params.name; + this.run = params.config; + } +} + +export function defineAppModule( + params: PulumiAppModuleParams +) { + return new PulumiAppModuleDefinition(params); +} diff --git a/packages/pulumi-sdk/src/index.ts b/packages/pulumi-sdk/src/index.ts index df08473512..74a94d1882 100644 --- a/packages/pulumi-sdk/src/index.ts +++ b/packages/pulumi-sdk/src/index.ts @@ -1,5 +1,6 @@ export * from "./Pulumi"; export * from "./PulumiApp"; +export * from "./PulumiAppModule"; export * from "./PulumiResource"; export * from "./ApplicationConfig"; export * from "./ApplicationHook";