diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be2a58a7..ecd8d852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,7 +233,17 @@ jobs: echo "DEPLOYER_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV echo "DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV echo "NEXTJS_DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV + echo "NEXTJS_DEMO_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV echo "RELEASE_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV + echo "RELEASE_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV + + - name: Echo Exports + run: | + env | grep EDGE_DOMAIN + env | grep DEPLOYER_ + env | grep DEMO_APP_ + env | grep NEXTJS_DEMO_APP_ + env | grep RELEASE_APP_ - name: Publish Demo App to MicroApps run: | @@ -327,7 +337,7 @@ jobs: -a ${NEXTJS_DEMO_APP_NAME} \ -n ${{ needs.build.outputs.nextjsDemoAppPackageVersion }} \ -d ${DEPLOYER_LAMBDA_NAME} \ - -l ${NEXTJS_DEMO_APP_LAMBDA_NAME} \ + -l ${NEXTJS_DEMO_APP_LAMBDA_VERSION_ARN} \ -s packages/cdk/node_modules/@pwrdrvr/microapps-app-nextjs-demo-cdk/lib/microapps-app-nextjs-demo/.static_files/ \ --overwrite --noCache @@ -358,7 +368,7 @@ jobs: -a ${RELEASE_APP_NAME} \ -n ${{ needs.build.outputs.releaseAppPackageVersion }} \ -d ${DEPLOYER_LAMBDA_NAME} \ - -l ${RELEASE_APP_LAMBDA_NAME} \ + -l ${RELEASE_APP_LAMBDA_VERSION_ARN} \ -s packages/cdk/node_modules/@pwrdrvr/microapps-app-release-cdk/lib/microapps-app-release/.static_files/ \ --overwrite --noCache diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts index 1022f0aa..4aee8d61 100644 --- a/packages/cdk/lib/MicroApps.ts +++ b/packages/cdk/lib/MicroApps.ts @@ -1,4 +1,4 @@ -import { Aws, CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; +import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as lambda from 'aws-cdk-lib/aws-lambda'; @@ -265,10 +265,17 @@ export class MicroAppsStack extends Stack { removalPolicy, }); + const appVersion = (app.lambdaFunction as lambda.Function).currentVersion; + new CfnOutput(this, 'release-app-func-name', { value: `${app.lambdaFunction.functionName}`, exportName: `${this.stackName}-release-app-func-name`, }); + + new CfnOutput(this, 'release-app-vers-arn', { + value: `${appVersion.functionArn}`, + exportName: `${this.stackName}-release-app-vers-arn`, + }); } if (deployNextjsDemoApp) { @@ -280,10 +287,17 @@ export class MicroAppsStack extends Stack { removalPolicy, }); + const appVersion = (app.lambdaFunction as lambda.Function).currentVersion; + new CfnOutput(this, 'nextjs-demo-app-func-name', { value: `${app.lambdaFunction.functionName}`, exportName: `${this.stackName}-nextjs-demo-app-func-name`, }); + + new CfnOutput(this, 'nextjs-demo-app-vers-arn', { + value: `${appVersion.functionArn}`, + exportName: `${this.stackName}-nextjs-demo-app-vers-arn`, + }); } // Exports diff --git a/packages/cdk/test/MicroApps.spec.ts b/packages/cdk/test/MicroApps.spec.ts index 603f8bb3..13c8e75d 100644 --- a/packages/cdk/test/MicroApps.spec.ts +++ b/packages/cdk/test/MicroApps.spec.ts @@ -15,7 +15,7 @@ describe('MicroAppsStack', () => { expect(stack).toBeDefined(); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {}); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); Template.fromStack(stack).hasOutput('edgedomainname', { Value: { 'Fn::GetAtt': ['microappscft5FDF8AB8', 'DomainName'] }, Export: { Name: `${stackName}-edge-domain-name` }, @@ -46,7 +46,7 @@ describe('MicroAppsStack', () => { expect(stack).toBeDefined(); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {}); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); Template.fromStack(stack).hasOutput('edgedomainname', { Value: 'appz.pwrdrvr.com', Export: { Name: `${stackName}-edge-domain-name` }, diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts index 61185b4c..7534d510 100644 --- a/packages/microapps-cdk/src/MicroAppsSvcs.ts +++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts @@ -188,7 +188,7 @@ export interface IMicroAppsSvcs { /** * Lambda function for the Router */ - readonly routerFunc: lambda.IFunction; + readonly routerFunc?: lambda.IFunction; } /** @@ -207,8 +207,8 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs { return this._deployerFunc; } - private _routerFunc: lambda.Function; - public get routerFunc(): lambda.IFunction { + private _routerFunc?: lambda.Function; + public get routerFunc(): lambda.IFunction | undefined { return this._routerFunc; } @@ -268,78 +268,6 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs { this._table = props.table; } - // - // Router Lambda Function - // - - // Create Router Lambda Function - const routerFuncProps: Omit = { - functionName: assetNameRoot ? `${assetNameRoot}-router${assetNameSuffix}` : undefined, - memorySize: 1769, - logRetention: logs.RetentionDays.ONE_MONTH, - runtime: lambda.Runtime.NODEJS_16_X, - timeout: Duration.seconds(15), - environment: { - NODE_ENV: appEnv, - DATABASE_TABLE_NAME: this._table.tableName, - AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', - ROOT_PATH_PREFIX: rootPathPrefix, - }, - }; - if ( - process.env.NODE_ENV === 'test' && - existsSync(path.join(__dirname, '..', '..', 'microapps-router', 'dist', 'index.js')) - ) { - // This is for local dev - this._routerFunc = new lambda.Function(this, 'router-func', { - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-router', 'dist')), - handler: 'index.handler', - ...routerFuncProps, - }); - } else if (existsSync(path.join(__dirname, 'microapps-router', 'index.js'))) { - // This is for built apps packaged with the CDK construct - this._routerFunc = new lambda.Function(this, 'router-func', { - code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-router')), - handler: 'index.handler', - ...routerFuncProps, - }); - } else { - // Create Router Lambda Layer - const routerDataFiles = new lambda.LayerVersion(this, 'router-templates', { - code: lambda.Code.fromAsset( - path.join(__dirname, '..', '..', 'microapps-router', 'templates'), - ), - removalPolicy, - }); - - this._routerFunc = new lambdaNodejs.NodejsFunction(this, 'router-func', { - entry: path.join(__dirname, '..', '..', 'microapps-router', 'src', 'index.ts'), - handler: 'handler', - bundling: { - minify: true, - sourceMap: true, - }, - layers: [routerDataFiles], - ...routerFuncProps, - }); - } - if (removalPolicy !== undefined) { - this._routerFunc.applyRemovalPolicy(removalPolicy); - } - const policyReadTarget = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: ['s3:GetObject'], - resources: [`${bucketApps.bucketArn}/*`], - }); - for (const router of [this._routerFunc]) { - router.addToRolePolicy(policyReadTarget); - // Give the Router access to DynamoDB table - this._table.grantReadData(router); - this._table.grant(router, 'dynamodb:DescribeTable'); - } - // Create alias for Router - const routerAlias = this._routerFunc.addAlias('CurrentVersion'); - // // Deployer Lambda Function // @@ -630,6 +558,78 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs { this._deployerFunc.addToRolePolicy(policyAPIManageLambdas); if (httpApi) { + // + // Router Lambda Function + // + + // Create Router Lambda Function + const routerFuncProps: Omit = { + functionName: assetNameRoot ? `${assetNameRoot}-router${assetNameSuffix}` : undefined, + memorySize: 1769, + logRetention: logs.RetentionDays.ONE_MONTH, + runtime: lambda.Runtime.NODEJS_16_X, + timeout: Duration.seconds(15), + environment: { + NODE_ENV: appEnv, + DATABASE_TABLE_NAME: this._table.tableName, + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', + ROOT_PATH_PREFIX: rootPathPrefix, + }, + }; + if ( + process.env.NODE_ENV === 'test' && + existsSync(path.join(__dirname, '..', '..', 'microapps-router', 'dist', 'index.js')) + ) { + // This is for local dev + this._routerFunc = new lambda.Function(this, 'router-func', { + code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-router', 'dist')), + handler: 'index.handler', + ...routerFuncProps, + }); + } else if (existsSync(path.join(__dirname, 'microapps-router', 'index.js'))) { + // This is for built apps packaged with the CDK construct + this._routerFunc = new lambda.Function(this, 'router-func', { + code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-router')), + handler: 'index.handler', + ...routerFuncProps, + }); + } else { + // Create Router Lambda Layer + const routerDataFiles = new lambda.LayerVersion(this, 'router-templates', { + code: lambda.Code.fromAsset( + path.join(__dirname, '..', '..', 'microapps-router', 'templates'), + ), + removalPolicy, + }); + + this._routerFunc = new lambdaNodejs.NodejsFunction(this, 'router-func', { + entry: path.join(__dirname, '..', '..', 'microapps-router', 'src', 'index.ts'), + handler: 'handler', + bundling: { + minify: true, + sourceMap: true, + }, + layers: [routerDataFiles], + ...routerFuncProps, + }); + } + if (removalPolicy !== undefined) { + this._routerFunc.applyRemovalPolicy(removalPolicy); + } + const policyReadTarget = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObject'], + resources: [`${bucketApps.bucketArn}/*`], + }); + for (const router of [this._routerFunc]) { + router.addToRolePolicy(policyReadTarget); + // Give the Router access to DynamoDB table + this._table.grantReadData(router); + this._table.grant(router, 'dynamodb:DescribeTable'); + } + // Create alias for Router + const routerAlias = this._routerFunc.addAlias('CurrentVersion'); + // This creates an integration and a router const route = new apigwy.HttpRoute(this, 'route-default', { httpApi, diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts index 168bdef4..2b113e27 100644 --- a/packages/microapps-cdk/test/MicroApps.test.ts +++ b/packages/microapps-cdk/test/MicroApps.test.ts @@ -28,7 +28,7 @@ describe('MicroApps', () => { expect(construct.node).toBeDefined(); Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1); Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); // Confirm that logical IDs have not changed accidentally (causes delete/create) Template.fromStack(stack).templateMatches({ @@ -42,9 +42,9 @@ describe('MicroApps', () => { // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: { // Type: 'AWS::Lambda::Function', // }, - constructsvcsrouterfunc73102284: { - Type: 'AWS::Lambda::Function', - }, + // constructsvcsrouterfunc73102284: { + // Type: 'AWS::Lambda::Function', + // }, constructs3apps91016270: { Type: 'AWS::S3::Bucket', }, @@ -90,7 +90,7 @@ describe('MicroApps', () => { expect(construct.node).toBeDefined(); Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1); Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); // Confirm that logical IDs have not changed accidentally (causes delete/create) Template.fromStack(stack).templateMatches({ @@ -104,9 +104,9 @@ describe('MicroApps', () => { // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: { // Type: 'AWS::Lambda::Function', // }, - constructsvcsrouterfunc73102284: { - Type: 'AWS::Lambda::Function', - }, + // constructsvcsrouterfunc73102284: { + // Type: 'AWS::Lambda::Function', + // }, constructs3apps91016270: { Type: 'AWS::S3::Bucket', }, @@ -153,10 +153,10 @@ describe('MicroApps', () => { expect(construct.node).toBeDefined(); Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1); Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); - Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { - FunctionName: 'my-asset-root-name-router-some-suffix', - }); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); + // Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + // FunctionName: 'my-asset-root-name-router-some-suffix', + // }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'my-asset-root-name-deployer-some-suffix', }); @@ -173,9 +173,9 @@ describe('MicroApps', () => { // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: { // Type: 'AWS::Lambda::Function', // }, - constructsvcsrouterfunc73102284: { - Type: 'AWS::Lambda::Function', - }, + // constructsvcsrouterfunc73102284: { + // Type: 'AWS::Lambda::Function', + // }, constructs3apps91016270: { Type: 'AWS::S3::Bucket', }, @@ -223,10 +223,10 @@ describe('MicroApps', () => { expect(construct.node).toBeDefined(); Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1); Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4); - Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { - FunctionName: 'my-asset-root-name-router-some-suffix', - }); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); + // Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + // FunctionName: 'my-asset-root-name-router-some-suffix', + // }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'my-asset-root-name-deployer-some-suffix', }); @@ -257,9 +257,9 @@ describe('MicroApps', () => { constructsvcsdeployerfunc88CC1526: { Type: 'AWS::Lambda::Function', }, - constructsvcsrouterfunc73102284: { - Type: 'AWS::Lambda::Function', - }, + // constructsvcsrouterfunc73102284: { + // Type: 'AWS::Lambda::Function', + // }, constructs3apps91016270: { Type: 'AWS::S3::Bucket', }, @@ -311,7 +311,7 @@ describe('MicroApps', () => { expect(construct.node).toBeDefined(); Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1); Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); - Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2); // Confirm that logical IDs have not changed accidentally (causes delete/create) Template.fromStack(stack).templateMatches({ @@ -322,9 +322,6 @@ describe('MicroApps', () => { constructsvcstable0311CF05: { Type: 'AWS::DynamoDB::Table', }, - constructsvcsrouterfunc73102284: { - Type: 'AWS::Lambda::Function', - }, constructs3apps91016270: { Type: 'AWS::S3::Bucket', }, diff --git a/packages/microapps-deployer/src/controllers/VersionController.spec.ts b/packages/microapps-deployer/src/controllers/VersionController.spec.ts index 7a245a78..ff81aff0 100644 --- a/packages/microapps-deployer/src/controllers/VersionController.spec.ts +++ b/packages/microapps-deployer/src/controllers/VersionController.spec.ts @@ -894,6 +894,109 @@ describe('VersionController', () => { expect(updatedVersion.RouteIDAppVersionSplat).toBe(''); }); + it('should 201 version that does not exist - w/ full arn w/version', async () => { + const appName = 'newapp'; + const semVer = '0.0.0'; + const fakeLambdaVersion = '3'; + const fakeLambdaARNBase = `arn:aws:lambda:${config.awsRegion}:${config.awsAccountID}:function:new-app-function`; + const fakeLambdaARN = `arn:aws:lambda:${config.awsRegion}:${config.awsAccountID}:function:new-app-function:${fakeLambdaVersion}`; + + s3Client + .onAnyCommand() + .rejects() + // .callsFake((input) => { + // console.log(`received input: ${JSON.stringify(input)}`); + // console.log( + // `our matcher: ${JSON.stringify({ + // Bucket: config.filestore.destinationBucket, + // CopySource: `${config.filestore.stagingBucket}${pathPrefix}/${appName}/${semVer}/index.html`, + // Key: `${pathPrefix}${appName}/${semVer}/index.html`, + // })}`, + // ); + // }) + // Mock S3 get for staging bucket - return one file name + .on(s3.ListObjectsV2Command, { + Bucket: config.filestore.stagingBucket, + Prefix: `${pathPrefix}${appName}/${semVer}/`, + }) + .resolves({ + IsTruncated: false, + Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }], + }) + // Mock S3 copy to prod bucket + .on(s3.CopyObjectCommand, { + Bucket: config.filestore.destinationBucket, + CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`, + Key: `${pathPrefix}${appName}/${semVer}/index.html`, + }) + .resolves({}) + .on(s3.DeleteObjectCommand, { + Bucket: config.filestore.stagingBucket, + Key: `${pathPrefix}${appName}/${semVer}/index.html`, + }) + .resolves({}); + + lambdaClient + .onAnyCommand() + .rejects() + .on(lambda.ListTagsCommand, { + Resource: fakeLambdaARNBase, + }) + .resolves({ + Tags: { + 'microapps-managed': 'true', + }, + }) + .on(lambda.TagResourceCommand, { + Resource: fakeLambdaARNBase, + Tags: { + 'microapp-managed': 'true', + }, + }) + .resolves({}) + .on(lambda.GetFunctionUrlConfigCommand, { + FunctionName: fakeLambdaARNBase, + Qualifier: fakeLambdaVersion, + }) + .resolves({ + FunctionUrl: fakeFunctionURLForAlias, + }); + apigwyClient.onAnyCommand().rejects(); + + const response = await handler( + { + appName: appName, + semVer: semVer, + defaultFile: 'index.html', + lambdaARN: fakeLambdaARN, + type: 'deployVersion', + overwrite: true, + appType: 'lambda-url', + startupType: 'direct', + url: 'https://abc1234567.lambda-url.us-east-1.on.aws', + } as IDeployVersionRequest, + { awsRequestId: '123' } as lambdaTypes.Context, + ); + expect(response.statusCode).toEqual(201); + + const updatedVersion = await Version.LoadVersion({ + dbManager, + key: { AppName: appName, SemVer: semVer }, + }); + // expect(updatedVersion).toEqual({}); + expect(updatedVersion.AppName).toBe(appName); + expect(updatedVersion.SemVer).toBe(semVer); + expect(updatedVersion.DefaultFile).toBe('index.html'); + expect(updatedVersion.LambdaARN).toBe(fakeLambdaARN); + expect(updatedVersion.URL).toBe(fakeFunctionURLForAlias); + expect(updatedVersion.StartupType).toBe('direct'); + expect(updatedVersion.Status).toBe('routed'); + expect(updatedVersion.Type).toBe('lambda-url'); + expect(updatedVersion.IntegrationID).toBe(''); + expect(updatedVersion.RouteIDAppVersion).toBe(''); + expect(updatedVersion.RouteIDAppVersionSplat).toBe(''); + }); + it('should 201 version that exists - overwrite true', async () => { const appName = 'newapp'; const semVer = '0.0.0'; diff --git a/packages/microapps-publish/README.md b/packages/microapps-publish/README.md index e905daec..d7fcf383 100644 --- a/packages/microapps-publish/README.md +++ b/packages/microapps-publish/README.md @@ -15,7 +15,6 @@ - [Command - nextjs-version](#command---nextjs-version) - [Command - nextjs-version-restore](#command---nextjs-version-restore) - [Command - delete](#command---delete) - - [Command - nextjs-docker-auto](#command---nextjs-docker-auto) # Video Preview of Deploying an App @@ -43,8 +42,6 @@ USAGE COMMANDS delete Delete app/version help display help for microapps-publish - nextjs-docker-auto Fully automatic publishing of Docker-based Lambda - function using Next.js and serverless-nextjs-router nextjs-version Apply version to next.config.js overtop of 0.0.0 placeholder nextjs-version-restore Restore next.config.js @@ -247,73 +244,3 @@ EXAMPLE $ microapps-publish delete -d microapps-deployer-dev -a release -n 0.0.13 ✔ App/Version deleted: release/0.0.13 [1.2s] ``` - -## Command - nextjs-docker-auto - -Note: semi-deprecated as of 2022-01-27. This command may still work but it performs too many tasks that needed to be split out into individual commands to allow for integration into various build processes. - -`npx microapps-publish nextjs-docker-auto help` - -``` -Fully automatic publishing of Docker-based Lambda function using Next.js and serverless-nextjs-router - -USAGE - $ microapps-publish nextjs-docker-auto - -OPTIONS - -a, --appName=appName - MicroApps app name - - -d, --deployerLambdaName=deployerLambdaName - (required) Name of the deployer lambda function - - -f, --leaveCopy - Leave a copy of the modifed files as .modified - - -i, --defaultFile=defaultFile - Default file to return when the app is loaded via the router without a - version (e.g. when app/ is requested). - - -l, --appLambdaName=appLambdaName - Name of the application lambda function - - -n, --newVersion=newVersion - (required) New semantic version to apply - - -o, --overwrite - Allow overwrite - Warn but do not fail if version exists. Discouraged - outside of test envs if cacheable static files have changed. - - -r, --repoName=repoName - (required) Name (not URI) of the Docker repo for the app - - -s, --staticAssetsPath=staticAssetsPath - Path to files to be uploaded to S3 static bucket at app/version/ path. Do - include app/version/ in path if files are already "rooted" under that path - locally. - - -v, --version - show CLI version - - --help - show CLI help - - --noCache - Force revalidation of CloudFront and browser caching of static assets - -EXAMPLE - $ microapps-publish nextjs-docker-auto -d microapps-deployer-dev -r - microapps-app-release-dev-repo -n 0.0.14 - ✔ Logging into ECR [2s] - ✔ Modifying Config Files [0.0s] - ✔ Preflight Version Check [1s] - ✔ Serverless Next.js Build [1m16s] - ✔ Publish to ECR [32s] - ✔ Deploy to Lambda [11s] - ✔ Confirm Static Assets Folder Exists [0.0s] - ✔ Copy Static Files to Local Upload Dir [0.0s] - ✔ Enumerate Files to Upload to S3 [0.0s] - ✔ Upload Static Files to S3 [1s] - ✔ Creating MicroApp Application: release [0.2s] - ✔ Creating MicroApp Version: 0.0.14 [1s] -``` diff --git a/packages/microapps-publish/src/commands/nextjs-docker-auto.ts b/packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts similarity index 99% rename from packages/microapps-publish/src/commands/nextjs-docker-auto.ts rename to packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts index 3f12bab3..82a02c72 100755 --- a/packages/microapps-publish/src/commands/nextjs-docker-auto.ts +++ b/packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts @@ -447,7 +447,10 @@ export class DockerAutoCommand extends Command { // Call Deployer to Deploy AppName/Version await DeployClient.DeployVersion({ - config, + appName: config.app.name, + semVer: config.app.semVer, + defaultFile: config.app.defaultFile, + deployerLambdaName: config.deployer.lambdaName, appType: 'lambda', overwrite, output: (message: string) => (task.output = message), diff --git a/packages/microapps-publish/src/commands/publish-static.ts b/packages/microapps-publish/src/commands/publish-static.ts index ab1ff43e..6d63b171 100644 --- a/packages/microapps-publish/src/commands/publish-static.ts +++ b/packages/microapps-publish/src/commands/publish-static.ts @@ -296,7 +296,10 @@ export class PublishCommand extends Command { // Call Deployer to Deploy AppName/Version await DeployClient.DeployVersion({ - config, + appName: config.app.name, + semVer: config.app.semVer, + deployerLambdaName: config.deployer.lambdaName, + defaultFile: config.app.defaultFile, appType: 'static', overwrite, output: (message: string) => (task.output = message), diff --git a/packages/microapps-publish/src/commands/publish.ts b/packages/microapps-publish/src/commands/publish.ts index cfa5cadb..3cc5f723 100644 --- a/packages/microapps-publish/src/commands/publish.ts +++ b/packages/microapps-publish/src/commands/publish.ts @@ -25,6 +25,16 @@ const lambdaClient = new lambda.LambdaClient({ interface IContext { preflightResult: IDeployVersionPreflightResult; files: string[]; + + /** + * The type of ARN passed in on the config or command line + */ + configLambdaArnType: 'function' | 'alias' | 'version'; + + /** + * The ARN of the Lambda alias to use for the deploy + */ + lambdaAliasArn: string; } export class PublishCommand extends Command { @@ -65,7 +75,7 @@ export class PublishCommand extends Command { char: 'l', multiple: false, required: false, - description: 'Name of the application lambda function', + description: 'ARN of lambda version, alias, or function (name or ARN) to deploy', }), appName: flagsParser.string({ char: 'a', @@ -213,8 +223,27 @@ export class PublishCommand extends Command { }, }, { - // TODO: Disable this task if no Lambda function - title: 'Deploy to Lambda', + title: 'Check if Lambda ARN has Alias', + task: (ctx, task) => { + if (appLambdaName.match(/:/g)?.length === 7) { + if (/^[0-9]$/.test(appLambdaName.substring(appLambdaName.lastIndexOf(':') + 1))) { + ctx.configLambdaArnType = 'version'; + task.output = `Lambda ARN has Version: ${config.app.lambdaName}`; + } else { + ctx.configLambdaArnType = 'alias'; + task.output = `Lambda ARN has Alias: ${config.app.lambdaName}`; + ctx.lambdaAliasArn = config.app.lambdaName; + } + } else { + ctx.configLambdaArnType = 'function'; + task.output = `Lambda ARN does not have Alias: ${config.app.lambdaName}`; + } + }, + }, + { + enabled: (ctx) => + ctx.configLambdaArnType === 'function' || ctx.configLambdaArnType === 'version', + title: 'Create Lambda Alias and, optionally, Version', task: async (ctx, task) => { // Allow overwriting a non-overwritable app if the prior // publish was not completely successful - in that case @@ -229,6 +258,7 @@ export class PublishCommand extends Command { versions: this.VersionAndAlias, overwrite: allowOverwrite, task, + ctx, }); task.title = origTitle; @@ -355,7 +385,11 @@ export class PublishCommand extends Command { // Call Deployer to Deploy AppName/Version await DeployClient.DeployVersion({ - config, + appName: config.app.name, + semVer: config.app.semVer, + deployerLambdaName: config.deployer.lambdaName, + lambdaAliasArn: ctx.lambdaAliasArn, + defaultFile: config.app.defaultFile, appType, startupType: parsedFlags['startup-type'] as 'iframe' | 'direct', overwrite, @@ -390,41 +424,51 @@ export class PublishCommand extends Command { versions: IVersions; overwrite: boolean; task: TaskWrapper; + ctx: IContext; }): Promise { - const { config, overwrite, versions, task } = opts; + const { config, overwrite, versions, task, ctx } = opts; - // Create Lambda version - task.output = 'Creating version for Lambda $LATEST'; - const resultUpdate = await lambdaClient.send( - new lambda.PublishVersionCommand({ - FunctionName: config.app.lambdaName, - }), - ); - const lambdaVersion = resultUpdate.Version; - task.output = `Lambda version created: ${resultUpdate.Version}`; - - let lastUpdateStatus = resultUpdate.LastUpdateStatus; - for (let i = 0; i < 5; i++) { - // When the function is created the status will be "Pending" - // and we have to wait until it's done creating - // before we can point an alias to it - if (lastUpdateStatus === 'Successful') { - task.output = `Lambda function updated, version: ${lambdaVersion}`; - break; - } - - // If it didn't work, wait and try again - await asyncSetTimeout(1000 * i); + let lambdaVersion = ''; + let lambdaArnBase = config.app.lambdaName; - const resultGet = await lambdaClient.send( - new lambda.GetFunctionCommand({ + // Create Lambda version + if (ctx.configLambdaArnType === 'function') { + task.output = 'Creating version for Lambda $LATEST'; + const resultUpdate = await lambdaClient.send( + new lambda.PublishVersionCommand({ FunctionName: config.app.lambdaName, - Qualifier: lambdaVersion, }), ); + lambdaVersion = resultUpdate.Version; + task.output = `Lambda version created: ${resultUpdate.Version}`; + + let lastUpdateStatus = resultUpdate.LastUpdateStatus; + for (let i = 0; i < 5; i++) { + // When the function is created the status will be "Pending" + // and we have to wait until it's done creating + // before we can point an alias to it + if (lastUpdateStatus === 'Successful') { + task.output = `Lambda function updated, version: ${lambdaVersion}`; + break; + } + + // If it didn't work, wait and try again + await asyncSetTimeout(1000 * i); + + const resultGet = await lambdaClient.send( + new lambda.GetFunctionCommand({ + FunctionName: config.app.lambdaName, + Qualifier: lambdaVersion, + }), + ); - // Save the last update status so we can check on re-loop - lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus; + // Save the last update status so we can check on re-loop + lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus; + } + } else { + task.output = 'Lambda is already versioned, skipping version creation'; + lambdaVersion = config.app.lambdaName.split(':')?.pop() || ''; + lambdaArnBase = config.app.lambdaName.substring(0, config.app.lambdaName.lastIndexOf(':')); } // Create Lambda alias point @@ -432,24 +476,26 @@ export class PublishCommand extends Command { try { const resultLambdaAlias = await lambdaClient.send( new lambda.CreateAliasCommand({ - FunctionName: config.app.lambdaName, + FunctionName: lambdaArnBase, Name: versions.alias, FunctionVersion: lambdaVersion, }), ); task.output = `Lambda alias created, name: ${resultLambdaAlias.Name}`; + ctx.lambdaAliasArn = resultLambdaAlias.AliasArn; } catch (error) { if (overwrite && error.name === 'ResourceConflictException') { task.output = `Alias exists, updating the lambda alias for version: ${lambdaVersion}`; const resultLambdaAlias = await lambdaClient.send( new lambda.UpdateAliasCommand({ - FunctionName: config.app.lambdaName, + FunctionName: lambdaArnBase, Name: versions.alias, FunctionVersion: lambdaVersion, }), ); task.output = `Lambda alias updated, name: ${resultLambdaAlias.Name}`; + ctx.lambdaAliasArn = resultLambdaAlias.AliasArn; } else { throw error; } diff --git a/packages/microapps-publish/src/config/Application.ts b/packages/microapps-publish/src/config/Application.ts index a1c01adc..1b0a7829 100644 --- a/packages/microapps-publish/src/config/Application.ts +++ b/packages/microapps-publish/src/config/Application.ts @@ -86,9 +86,11 @@ export class ApplicationConfig implements IApplicationConfig { }) public lambdaName: string; public get lambdaARN(): string { - return `arn:aws:lambda:${this.awsRegion}:${this.awsAccountID}:function:${ - this.lambdaName - }:v${this.semVer.replace(/\./g, '_')}`; + return this.lambdaName.includes(':') + ? this.lambdaName + : `arn:aws:lambda:${this.awsRegion}:${this.awsAccountID}:function:${ + this.lambdaName + }:v${this.semVer.replace(/\./g, '_')}`; } @convict.Property({ diff --git a/packages/microapps-publish/src/lib/DeployClient.ts b/packages/microapps-publish/src/lib/DeployClient.ts index f3cdeea5..083104df 100644 --- a/packages/microapps-publish/src/lib/DeployClient.ts +++ b/packages/microapps-publish/src/lib/DeployClient.ts @@ -147,26 +147,40 @@ export default class DeployClient { * @param task */ public static async DeployVersion(opts: { - config: IConfig; + appName: string; + semVer: string; + defaultFile?: string; + lambdaAliasArn?: string; + deployerLambdaName: string; appType: 'lambda' | 'static' | 'lambda-url' | 'url'; startupType?: 'iframe' | 'direct'; overwrite: boolean; output: (message: string) => void; }): Promise { - const { config, appType, startupType = 'iframe', overwrite, output } = opts; + const { + appName, + semVer, + defaultFile, + lambdaAliasArn, + deployerLambdaName, + appType, + startupType = 'iframe', + overwrite, + output, + } = opts; const request: IDeployVersionRequest = { type: 'deployVersion', appType, startupType, - appName: config.app.name, - semVer: config.app.semVer, - defaultFile: config.app.defaultFile, + appName: appName, + semVer: semVer, + defaultFile: defaultFile, overwrite, - ...(['lambda', 'lambda-url'].includes(appType) ? { lambdaARN: config.app.lambdaARN } : {}), + ...(['lambda', 'lambda-url'].includes(appType) ? { lambdaARN: lambdaAliasArn } : {}), }; const response = await this._client.send( new lambda.InvokeCommand({ - FunctionName: config.deployer.lambdaName, + FunctionName: deployerLambdaName, Payload: Buffer.from(JSON.stringify(request)), }), ); @@ -176,7 +190,7 @@ export default class DeployClient { Buffer.from(response.Payload).toString('utf-8'), ) as IDeployerResponse; if (dResponse.statusCode === 201) { - output(`Deploy succeeded: ${config.app.name}/${config.app.semVer}`); + output(`Deploy succeeded: ${appName}/${semVer}`); } else { output(`Deploy failed with: ${dResponse.statusCode}`); throw new Error(`Lambda call to DeployVersionfailed with: ${dResponse.statusCode}`);