-
Notifications
You must be signed in to change notification settings - Fork 820
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: FF for override stacks (#8228)
- Loading branch information
Showing
37 changed files
with
746 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
packages/amplify-cli-core/src/category-interfaces/amplify-base-cdk-types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
123 changes: 123 additions & 0 deletions
123
packages/amplify-cli-core/src/category-interfaces/category-base-schema-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | ||
}; |
12 changes: 12 additions & 0 deletions
12
packages/amplify-cli-core/src/category-interfaces/category-input-state.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
24 changes: 24 additions & 0 deletions
24
packages/amplify-cli-core/src/category-interfaces/category-override-base.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './override-skeleton-generator'; |
81 changes: 81 additions & 0 deletions
81
packages/amplify-cli-core/src/overrides-manager/override-skeleton-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'))); | ||
}; |
Oops, something went wrong.