Skip to content

Commit

Permalink
microapps-publish - Optional app overwrite (#154)
Browse files Browse the repository at this point in the history
* Optional App Overwrite

* Overwrite param Deployer requests

* Update docs for overwrite param

* Actually pass overwrite flag to Deployer

* Set defaultFile again on overwrite

* Add noCache flag to microapps-publish
  • Loading branch information
huntharo authored Dec 2, 2021
1 parent 36f0bce commit 854185e
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 160 deletions.
22 changes: 22 additions & 0 deletions packages/microapps-deployer-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,35 @@ export interface IRequestBase {

export interface ICreateApplicationRequest extends IRequestBase {
readonly type: 'createApp';

/**
* Name of the application
*/
readonly appName: string;

/**
* Display name of the application
*/
readonly displayName: string;
}

export interface IDeployVersionRequestBase extends IRequestBase {
/**
* Name of the application
*/
readonly appName: string;

/**
* SemVer being published
*/
readonly semVer: string;

/**
* Allow overwrite of existing version
*
* @default false;
*/
readonly overwrite?: boolean;
}

export interface IDeployVersionPreflightRequest extends IDeployVersionRequestBase {
Expand Down
160 changes: 92 additions & 68 deletions packages/microapps-deployer/src/controllers/VersionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,77 +41,88 @@ export default class VersionController {
config: IConfig;
}): Promise<IDeployVersionPreflightResponse> {
const { dbManager, request, config } = opts;
const { appName, semVer, needS3Creds = true } = request;
const { appName, semVer, needS3Creds = true, overwrite = false } = request;

// Check if the version exists
const record = await Version.LoadVersion({
dbManager,
key: { AppName: appName, SemVer: semVer },
});
if (record !== undefined && record.Status !== 'pending') {
//
// Version exists and has moved beyond pending status
// No need to create S3 upload credentials
// NOTE: This may change in the future if we allow
// mutability of versions (at own risk)
//
Log.Instance.info('App/Version already exists', { appName, semVer });
return { statusCode: 200 };
} else {
//
// Version does not exist
// Create S3 temp credentials for the static assets upload
//

Log.Instance.info('App/Version does not exist', { appName, semVer });

// Get S3 creds if requested
if (needS3Creds) {
// Generate a temp policy for staging bucket app prefix
const iamPolicyDoc = new iamCDK.PolicyDocument({
statements: [
new iamCDK.PolicyStatement({
effect: iamCDK.Effect.ALLOW,
actions: ['s3:PutObject', 's3:GetObject', 's3:AbortMultipartUpload'],
resources: [`arn:aws:s3:::${config.filestore.stagingBucket}/*`],
}),
new iamCDK.PolicyStatement({
effect: iamCDK.Effect.ALLOW,
actions: ['s3:ListBucket'],
resources: [`arn:aws:s3:::${config.filestore.stagingBucket}`],
}),
],
if (!overwrite) {
//
// Version exists and has moved beyond pending status
// No need to create S3 upload credentials
// NOTE: This may change in the future if we allow
// mutability of versions (at own risk)
//
Log.Instance.info('Error: App/Version already exists', {
appName: request.appName,
semVer: request.semVer,
});

Log.Instance.debug('Temp IAM Policy', { policy: JSON.stringify(iamPolicyDoc.toJSON()) });
return { statusCode: 200 };
} else {
Log.Instance.info('Warning: App/Version already exists', {
appName: request.appName,
semVer: request.semVer,
});
}
}

// Assume the upload role with limited S3 permissions
const stsResult = await stsClient.send(
new sts.AssumeRoleCommand({
RoleArn: `arn:aws:iam::${config.awsAccountID}:role/${config.uploadRoleName}`,
DurationSeconds: 60 * 60,
RoleSessionName: VersionController.SHA1Hash(VersionController.GetBucketPrefix(request)),
Policy: JSON.stringify(iamPolicyDoc.toJSON()),
//
// Version does not exist
// Create S3 temp credentials for the static assets upload
//

Log.Instance.info('App/Version does not exist', { appName, semVer });

// Get S3 creds if requested
if (needS3Creds) {
// Generate a temp policy for staging bucket app prefix
const iamPolicyDoc = new iamCDK.PolicyDocument({
statements: [
new iamCDK.PolicyStatement({
effect: iamCDK.Effect.ALLOW,
actions: ['s3:PutObject', 's3:GetObject', 's3:AbortMultipartUpload'],
resources: [`arn:aws:s3:::${config.filestore.stagingBucket}/*`],
}),
);
new iamCDK.PolicyStatement({
effect: iamCDK.Effect.ALLOW,
actions: ['s3:ListBucket'],
resources: [`arn:aws:s3:::${config.filestore.stagingBucket}`],
}),
],
});

return {
statusCode: 404,
s3UploadUrl: `s3://${config.filestore.stagingBucket}/${VersionController.GetBucketPrefix(
request,
)}`,

awsCredentials: {
accessKeyId: stsResult.Credentials?.AccessKeyId as string,
secretAccessKey: stsResult.Credentials?.SecretAccessKey as string,
sessionToken: stsResult.Credentials?.SessionToken as string,
},
};
} else {
return {
statusCode: 404,
};
}
Log.Instance.debug('Temp IAM Policy', { policy: JSON.stringify(iamPolicyDoc.toJSON()) });

// Assume the upload role with limited S3 permissions
const stsResult = await stsClient.send(
new sts.AssumeRoleCommand({
RoleArn: `arn:aws:iam::${config.awsAccountID}:role/${config.uploadRoleName}`,
DurationSeconds: 60 * 60,
RoleSessionName: VersionController.SHA1Hash(VersionController.GetBucketPrefix(request)),
Policy: JSON.stringify(iamPolicyDoc.toJSON()),
}),
);

return {
statusCode: 404,
s3UploadUrl: `s3://${config.filestore.stagingBucket}/${VersionController.GetBucketPrefix(
request,
)}`,

awsCredentials: {
accessKeyId: stsResult.Credentials?.AccessKeyId as string,
secretAccessKey: stsResult.Credentials?.SecretAccessKey as string,
sessionToken: stsResult.Credentials?.SessionToken as string,
},
};
} else {
return {
statusCode: 404,
};
}
}

Expand All @@ -126,6 +137,8 @@ export default class VersionController {
config: IConfig;
}): Promise<IDeployerResponse> {
const { dbManager, request, config } = opts;
const { overwrite = false } = request;

Log.Instance.debug('Got Body:', request);

const destinationPrefix = VersionController.GetBucketPrefix(request);
Expand All @@ -136,11 +149,19 @@ export default class VersionController {
key: { AppName: request.appName, SemVer: request.semVer },
});
if (record !== undefined && record.Status === 'routed') {
Log.Instance.info('App/Version already exists', {
appName: request.appName,
semVer: request.semVer,
});
return { statusCode: 409 };
if (!overwrite) {
Log.Instance.info('Error: App/Version already exists', {
appName: request.appName,
semVer: request.semVer,
});

return { statusCode: 409 };
} else {
Log.Instance.info('Warning: App/Version already exists', {
appName: request.appName,
semVer: request.semVer,
});
}
}

const { appType = 'lambda' } = request;
Expand All @@ -161,7 +182,7 @@ export default class VersionController {
}

// Only copy the files if not copied yet
if (record.Status === 'pending') {
if (overwrite || record.Status === 'pending') {
const { stagingBucket } = config.filestore;
const sourcePrefix = VersionController.GetBucketPrefix(request) + '/';

Expand All @@ -174,6 +195,9 @@ export default class VersionController {
config,
);

// Set defaultFile again in-case this is an overwrite
record.DefaultFile = request.defaultFile;

// Update status to assets-copied
record.Status = 'assets-copied';
await record.Save(dbManager);
Expand All @@ -185,7 +209,7 @@ export default class VersionController {
// Get the API Gateway
const apiId = config.apigwy.apiId;

if (record.Status === 'assets-copied') {
if (overwrite || record.Status === 'assets-copied') {
// Get the account ID and region for API Gateway to Lambda permissions
const accountId = config.awsAccountID;
const region = config.awsRegion;
Expand Down Expand Up @@ -235,7 +259,7 @@ export default class VersionController {

// Add Integration pointing to Lambda Function Alias
let integrationId = '';
if (record.Status === 'permissioned') {
if (overwrite || record.Status === 'permissioned') {
if (record.IntegrationID !== undefined && record.IntegrationID !== '') {
integrationId = record.IntegrationID;
} else {
Expand Down Expand Up @@ -269,7 +293,7 @@ export default class VersionController {
}
}

if (record.Status === 'integrated') {
if (overwrite || record.Status === 'integrated') {
// Add the routes to API Gateway for appName/version/{proxy+}
try {
await apigwyClient.send(
Expand Down
59 changes: 42 additions & 17 deletions packages/microapps-publish/src/commands/nextjs-docker-auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import * as lambda from '@aws-sdk/client-lambda';
import * as s3 from '@aws-sdk/client-s3';
import * as sts from '@aws-sdk/client-sts';
import { Command, flags as flagsParser } from '@oclif/command';
import * as chalk from 'chalk';
import * as path from 'path';
import { promises as fs, pathExists, createReadStream } from 'fs-extra';
import { Listr, ListrTask } from 'listr2';
Expand Down Expand Up @@ -112,6 +111,18 @@ export class DockerAutoCommand extends Command {
description:
'Default file to return when the app is loaded via the router without a version (e.g. when app/ is requested).',
}),
overwrite: flagsParser.boolean({
char: 'o',
required: false,
default: false,
description:
'Allow overwrite - Warn but do not fail if version exists. Discouraged outside of test envs if cacheable static files have changed.',
}),
noCache: flagsParser.boolean({
required: false,
default: false,
description: 'Force revalidation of CloudFront and browser caching of static assets',
}),
};

private VersionAndAlias: IVersions;
Expand All @@ -136,6 +147,8 @@ export class DockerAutoCommand extends Command {
const ecrRepo = parsedFlags.repoName ?? config.app.ecrRepoName;
const staticAssetsPath = parsedFlags.staticAssetsPath ?? config.app.staticAssetsPath;
const defaultFile = parsedFlags.defaultFile ?? config.app.defaultFile;
const overwrite = parsedFlags.overwrite;
const noCache = parsedFlags.noCache;

// Override the config value
config.deployer.lambdaName = deployerLambdaName;
Expand Down Expand Up @@ -189,11 +202,8 @@ export class DockerAutoCommand extends Command {
await restoreFiles(this.FILES_TO_MODIFY);
});

if (config === undefined) {
this.error('Failed to load the config file');
}
if (config.app.staticAssetsPath === undefined) {
this.error('StaticAssetsPath must be specified in the config file');
this.error('staticAssetsPath must be specified');
}

//
Expand Down Expand Up @@ -240,13 +250,20 @@ export class DockerAutoCommand extends Command {
task.output = `Checking if deployed app/version already exists for ${config.app.name}/${semVer}`;
ctx.preflightResult = await DeployClient.DeployVersionPreflight({
config,
overwrite,
output: (message: string) => (task.output = message),
});
if (ctx.preflightResult.exists) {
task.output = `Warning: App/Version already exists: ${config.app.name}/${config.app.semVer}`;
if (!overwrite) {
throw new Error(
`App/Version already exists: ${config.app.name}/${config.app.semVer}`,
);
} else {
task.title = `Warning: App/Version already exists: ${config.app.name}/${config.app.semVer}`;
}
} else {
task.title = `App/Version does not exist: ${config.app.name}/${config.app.semVer}`;
}

task.title = origTitle;
},
},
{
Expand Down Expand Up @@ -353,6 +370,16 @@ export class DockerAutoCommand extends Command {
},
});

// Setup caching on static assets
// NoCache - Only used for test deploys, requires browser and CloudFront to refetch every time
// Overwrite - Reduces default cache time period from 24 hours to 15 minutes
// Default - 24 hours
const CacheControl = noCache
? 'max-age=0, must-revalidate, public'
: overwrite
? `max-age=${15 * 60}, public`
: `max-age=${24 * 60 * 60}, public`;

const pathWithoutAppAndVer = path.join(S3Uploader.TempDir, destinationPrefix);

const tasks: ListrTask<IContext>[] = ctx.files.map((filePath) => ({
Expand All @@ -370,7 +397,7 @@ export class DockerAutoCommand extends Command {
Key: path.relative(S3Uploader.TempDir, filePath),
Body: createReadStream(filePath),
ContentType: contentType(path.basename(filePath)) || 'application/octet-stream',
CacheControl: 'max-age=86400; public',
CacheControl,
},
});
await upload.done();
Expand Down Expand Up @@ -398,7 +425,7 @@ export class DockerAutoCommand extends Command {
task.title = RUNNING + origTitle;

// Call Deployer to Create App if Not Exists
await DeployClient.CreateApp(config);
await DeployClient.CreateApp({ config });

task.title = origTitle;
},
Expand All @@ -410,11 +437,12 @@ export class DockerAutoCommand extends Command {
task.title = RUNNING + origTitle;

// Call Deployer to Deploy AppName/Version
await DeployClient.DeployVersion(
await DeployClient.DeployVersion({
config,
'lambda',
(message: string) => (task.output = message),
);
appType: 'lambda',
overwrite,
output: (message: string) => (task.output = message),
});

task.title = origTitle;
},
Expand All @@ -429,9 +457,6 @@ export class DockerAutoCommand extends Command {

try {
await tasks.run();
// this.log(`Published: ${config.app.name}/${config.app.semVer}`);
} catch (error) {
this.log(`Caught exception: ${error.message}`);
} finally {
await S3Uploader.removeTempDirIfExists();
await restoreFiles(this.FILES_TO_MODIFY);
Expand Down
Loading

0 comments on commit 854185e

Please sign in to comment.