Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

microapps-publish - Optional app overwrite #154

Merged
merged 6 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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