Skip to content

Commit

Permalink
feat: FF for override stacks (#8228)
Browse files Browse the repository at this point in the history
  • Loading branch information
akshbhu authored Sep 25, 2021
1 parent f2fa479 commit 49ba6a3
Show file tree
Hide file tree
Showing 37 changed files with 746 additions and 20 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ module.exports = {
'/packages/amplify-graphql-*transformer*/lib',
'/packages/amplify-provider-awscloudformation/lib',
'/packages/amplify-console-integration-tests/lib',
'/packages/amplify-cli-overrides-helper/lib',

// Ignore CHANGELOG.md files
'/packages/*/CHANGELOG.md',
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ packages/**/reports/junit/*
test.out.log
*.tsbuildinfo
packages/amplify-graphiql-explorer/.eslintcache
packages/amplify-cli-overrides-helper/lib

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//Define all common classes and interfaces required to generate cloudformation using CDK.
import * as cdk from '@aws-cdk/core';

//Base template
//Customer can use these params to mutate the Cloudformation for the resource
export interface AmplifyStackTemplate {
addCfnParameter(props: cdk.CfnParameterProps, logicalId: string): void;
addCfnOutput(props: cdk.CfnOutputProps, logicalId: string): void;
addCfnMapping(props: cdk.CfnMappingProps, logicalId: string): void;
addCfnCondition(props: cdk.CfnConditionProps, logicalId: string): void;

getCfnParameter(logicalId: string): cdk.CfnParameter;
getCfnOutput(logicalId: string): cdk.CfnOutput;
getCfnMapping(logicalId: string): cdk.CfnMapping;
getCfnCondition(logicalId: string): cdk.CfnCondition;
}

export interface Template {
AWSTemplateFormatVersion?: string;
Description?: string;
Metadata?: Record<string, any>;
Parameters?: Record<string, any>;
Mappings?: {
[key: string]: {
[key: string]: Record<string, string | number | string[]>;
};
};
Conditions?: Record<string, any>;
Transform?: any;
Resources?: Record<string, any>;
Outputs?: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Utility base classes for all categories : CLIInputSchemaGenerator
* Generates JSON-schema from Typescript structures.The generated schemas
* can be used for run-time validation of Walkthrough/Headless structures.
*/
import { getProgramFromFiles, buildGenerator, PartialArgs } from 'typescript-json-schema';
import fs from 'fs-extra';
import path from 'path';
import Ajv from 'ajv';
import { printer } from 'amplify-prompts';

// Interface types are expected to be exported as "typeName" in the file
export type TypeDef = {
typeName: string;
service: string;
};

export class CLIInputSchemaGenerator {
// Paths are relative to the package root
TYPES_SRC_ROOT = './src/provider-utils/awscloudformation/service-walkthrough-types/';
SCHEMA_FILES_ROOT = './resources/schemas';
OVERWRITE_SCHEMA_FLAG = '--overwrite';

private serviceTypeDefs: TypeDef[];

private getSchemaFileNameForType(typeName: string): string {
return `${typeName}.schema.json`;
}

private getTypesSrcRootForSvc(svcName: string): string {
return `${this.TYPES_SRC_ROOT}/${svcName}-user-input-types.ts`;
}

private getSvcFileAbsolutePath(svcName: string): string {
return path.resolve(this.getTypesSrcRootForSvc(svcName));
}

private printWarningSchemaFileExists() {
printer.info('The interface version must be bumped after any changes.');
printer.info(`Use the ${this.OVERWRITE_SCHEMA_FLAG} flag to overwrite existing versions`);
printer.info('Skipping this schema');
}

private printSuccessSchemaFileWritten(typeName: string) {
printer.info(`Schema written for type ${typeName}.`);
}

constructor(typeDefs: TypeDef[]) {
this.serviceTypeDefs = typeDefs;
}

public generateJSONSchemas(): string[] {
const force = process.argv.includes(this.OVERWRITE_SCHEMA_FLAG);
const generatedFilePaths: string[] = [];

// schema generation settings. see https://www.npmjs.com/package/typescript-json-schema#command-line
const settings: PartialArgs = {
required: true,
};

for (const typeDef of this.serviceTypeDefs) {
//get absolute file path to the user-input types for the given service
const svcAbsoluteFilePath = this.getSvcFileAbsolutePath(typeDef.service);
printer.info(svcAbsoluteFilePath);
//generate json-schema from the input-types
const typeSchema = buildGenerator(getProgramFromFiles([svcAbsoluteFilePath]), settings)?.getSchemaForSymbol(typeDef.typeName);
//save json-schema file for the input-types. (used to validate cli-inputs.json)
const outputSchemaFilePath = path.resolve(
path.join(this.SCHEMA_FILES_ROOT, typeDef.service, this.getSchemaFileNameForType(typeDef.typeName)),
);
if (!force && fs.existsSync(outputSchemaFilePath)) {
this.printWarningSchemaFileExists();
return generatedFilePaths;
}
fs.ensureFileSync(outputSchemaFilePath);
fs.writeFileSync(outputSchemaFilePath, JSON.stringify(typeSchema, undefined, 4));
//print success status to the terminal
this.printSuccessSchemaFileWritten(typeDef.typeName);
generatedFilePaths.push(outputSchemaFilePath);
}
return generatedFilePaths;
}
}

//Read Schema, Validate and return Typescript object.
export class CLIInputSchemaValidator {
_category: string;
_service: string;
_schemaFileName: string;
_ajv: Ajv.Ajv;

constructor(service: string, category: string, schemaFileName: string) {
this._category = category;
this._service = service;
this._schemaFileName = schemaFileName;
this._ajv = new Ajv();
}

async getUserInputSchema() {
try {
return await import(generateSchemaPath(this._category, this._service, this._schemaFileName));
} catch (ex) {
throw new Error(`Schema defination doesnt exist : ${generateSchemaPath(this._category, this._service, this._schemaFileName)}`);
}
}

async validateInput(userInput: string): Promise<boolean> {
const userInputSchema = await this.getUserInputSchema();
if (userInputSchema.dependencySchemas) {
userInputSchema.dependencySchemas.reduce((acc: { addSchema: (arg0: any) => any }, it: any) => acc.addSchema(it), this._ajv);
}
const validate = this._ajv.compile(userInputSchema);
const input = JSON.parse(userInput);
if (!validate(input) as boolean) {
throw new Error(`Data did not validate against the supplied schema. Underlying errors were ${JSON.stringify(validate.errors)}`);
}
return true;
}
}

const generateSchemaPath = (category: string, service: string, schemaFileName: string): string => {
return path.join(`@aws-amplify/amplify-category-${category}`, 'resources', 'schemas', `${service}`, `${schemaFileName}.schema.json`);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { $TSAny } from '..';

export abstract class CategoryInputState {
_resourceName: string;
constructor(resourceName: string) {
this._resourceName = resourceName;
}

abstract getCLIInputPayload(): $TSAny;
abstract saveCLIInputPayload(props: $TSAny): void;
abstract isCLIInputsValid(props: $TSAny): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { $TSContext } from '..';
import { Template } from './amplify-base-cdk-types';

export abstract class AmplifyCategoryTransform {
_resourceName: string;
constructor(resourceName: string) {
this._resourceName = resourceName;
}
/**
* Entry point for CFN transformation process for a category
* @param context
*/
abstract transform(context: $TSContext): Promise<Template>;
/**
* Apply overrides on the derviced class object
*/
abstract applyOverride(): Promise<void>;
/**
* This function build and write CFN files and parameters.json to disk
* @param context amplify context
* @param template Amplify generated CFN template
*/
abstract saveBuildFiles(context: $TSContext, template: Template): Promise<void>;
}
4 changes: 4 additions & 0 deletions packages/amplify-cli-core/src/category-interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './amplify-base-cdk-types';
export * from './category-override-base';
export * from './category-base-schema-generator';
export * from './category-input-state';
34 changes: 34 additions & 0 deletions packages/amplify-cli-core/src/cliConstants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
export const SecretFileMode = 0o600; //file permissions for -rw-------
export const CLISubCommands = {
ADD: 'add',
PUSH: 'push',
PULL: 'pull',
REMOVE: 'remove',
UPDATE: 'update',
CONSOLE: 'console',
IMPORT: 'import',
};
export const AmplifyCategories = {
STORAGE: 'storage',
API: 'api',
AUTH: 'auth',
FUNCTION: 'function',
HOSTING: 'hosting',
INTERACTIONS: 'interactions',
NOTIFICATIONS: 'notifications',
PREDICTIONS: 'predictions',
ANALYTICS: 'analytics',
};

export const AmplifySupportedService = {
S3: 'S3',
DYNAMODB: 'DynamoDB',
COGNITO: 'Cognito',
};

export const overriddenCategories = [AmplifyCategories.AUTH];

export type IAmplifyResource = {
category: string;
resourceName: string;
service: string;
};
16 changes: 16 additions & 0 deletions packages/amplify-cli-core/src/feature-flags/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,5 +699,21 @@ export class FeatureFlags {
defaultValueForNewProjects: 1,
},
]);

// FF for overrides
this.registerFlag('overrides', [
{
name: 'auth',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
{
name: 'project',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
]);
};
}
3 changes: 3 additions & 0 deletions packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export * from './banner-message';
export * from './cliGetCategories';
export * from './cliRemoveResourcePrompt';
export * from './cliViewAPI';
export * from './overrides-manager';
export * from './hooks';
export * from './cliConstants';
export * from './category-interfaces';

// Temporary types until we can finish full type definition across the whole CLI

Expand Down
1 change: 1 addition & 0 deletions packages/amplify-cli-core/src/overrides-manager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './override-skeleton-generator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import fs from 'fs-extra';
import { $TSContext, getPackageManager } from '../index';
import execa from 'execa';
import * as path from 'path';
import { printer } from 'amplify-prompts';
import { JSONUtilities } from '../jsonUtilities';

export const generateOverrideSkeleton = async (context: $TSContext, srcResourceDirPath: string, destDirPath: string): Promise<void> => {
// 1. Create skeleton package
const backendDir = context.amplify.pathManager.getBackendDirPath();
const overrideFile = path.join(destDirPath, 'override.ts');
if (fs.existsSync(overrideFile)) {
await context.amplify.openEditor(context, overrideFile);
return;
}
// add tsConfig and package.json to amplify/backend
generateAmplifyOverrideProjectBuildFiles(backendDir, srcResourceDirPath);

fs.ensureDirSync(destDirPath);

// add overrde.ts and tsconfig<project> to build folder of the resource / rootstack
generateTsConfigforProject(backendDir, srcResourceDirPath, destDirPath);

// 2. Build Override Directory
await buildOverrideDir(backendDir, destDirPath);

printer.success(`Successfully generated "override.ts" folder at ${destDirPath}`);
await context.amplify.openEditor(context, overrideFile);
};

export async function buildOverrideDir(cwd: string, destDirPath: string) {
const packageManager = getPackageManager(cwd);

if (packageManager === null) {
throw new Error('No package manager found. Please install npm or yarn to compile overrides for this project.');
}

try {
execa.sync(packageManager.executable, ['install'], {
cwd,
stdio: 'pipe',
encoding: 'utf-8',
});
} catch (error) {
if ((error as any).code === 'ENOENT') {
throw new Error(`Packaging overrides failed. Could not find ${packageManager} executable in the PATH.`);
} else {
throw new Error(`Packaging overrides failed with the error \n${error.message}`);
}
}

// run tsc build to build override.ts file
const tsConfigDir = path.join(destDirPath, 'build');
const tsConfigFilePath = path.join(tsConfigDir, 'tsconfig.resource.json');
execa.sync('tsc', [`--project`, `${tsConfigFilePath}`], {
cwd: tsConfigDir,
stdio: 'pipe',
encoding: 'utf-8',
});
}

export const generateAmplifyOverrideProjectBuildFiles = (backendDir: string, srcResourceDirPath: string) => {
const packageJSONFilePath = path.join(backendDir, 'package.json');
const tsConfigFilePath = path.join(backendDir, 'tsconfig.json');
// add package.json to amplofy backend
if (!fs.existsSync(packageJSONFilePath)) {
JSONUtilities.writeJson(packageJSONFilePath, JSONUtilities.readJson(path.join(srcResourceDirPath, 'package.json')));
}

// add tsConfig.json to amplify backend
if (!fs.existsSync(tsConfigFilePath)) {
JSONUtilities.writeJson(tsConfigFilePath, JSONUtilities.readJson(path.join(srcResourceDirPath, 'tsconfig.json')));
}
};

export const generateTsConfigforProject = (backendDir: string, srcResourceDirPath: string, destDirPath: string) => {
const overrideFileName = path.join(destDirPath, 'override.ts');
const resourceTsConfigFileName = path.join(destDirPath, 'build', 'tsconfig.resource.json');
fs.writeFileSync(overrideFileName, fs.readFileSync(path.join(srcResourceDirPath, 'override.ts')));
fs.writeFileSync(resourceTsConfigFileName, fs.readFileSync(path.join(srcResourceDirPath, 'tsconfig.resource.json')));
};
Loading

0 comments on commit 49ba6a3

Please sign in to comment.