Skip to content

Commit

Permalink
fix(amplify-provider-awscloudformation): use prev deployment vars (aw…
Browse files Browse the repository at this point in the history
…s-amplify#6486)

* fix(amplify-provider-awscloudformation): use prev deployment vars

create deployment record which includes previously deployed parameters and capabilities - add action
to bubble up error within state machine

* test(amplify-e2e-tests): updated iterative e2e

updated iterative e2e to check for iam auth when making iterative changes

* updated pr feedback and iterative e2e to use api add and update for multi auth

* Update packages/amplify-provider-awscloudformation/src/iterative-deployment/deployment-manager.ts

Co-authored-by: Yathi <[email protected]>

Co-authored-by: Yathi <[email protected]>
  • Loading branch information
Josue Ruiz and yuth authored Jan 28, 2021
1 parent 525deb6 commit 39dfd27
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 32 deletions.
26 changes: 12 additions & 14 deletions packages/amplify-e2e-core/src/categories/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function getSchemaPath(schemaName: string): string {
}

export function apiGqlCompile(cwd: string, testingWithLatestCodebase: boolean = false) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(testingWithLatestCodebase), ['api', 'gql-compile'], { cwd, stripColors: true })
.wait('GraphQL schema compiled successfully.')
.run((err: Error) => {
Expand All @@ -32,12 +32,12 @@ const defaultOptions: AddApiOptions = {

export function addApiWithoutSchema(cwd: string, opts: Partial<AddApiOptions> = {}) {
const options = _.assign(defaultOptions, opts);
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
.wait('Provide API name:')
.sendLine(opts.apiName)
.sendLine(options.apiName)
.wait(/.*Choose the default authorization type for the API.*/)
.sendCarriageReturn()
.wait(/.*Enter a description for the API key.*/)
Expand Down Expand Up @@ -68,7 +68,7 @@ export function addApiWithoutSchema(cwd: string, opts: Partial<AddApiOptions> =
export function addApiWithSchema(cwd: string, schemaFile: string, opts: Partial<AddApiOptions & { apiKeyExpirationDays: number }> = {}) {
const options = _.assign(defaultOptions, opts);
const schemaPath = getSchemaPath(schemaFile);
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -101,7 +101,7 @@ export function addApiWithSchema(cwd: string, schemaFile: string, opts: Partial<

export function addApiWithSchemaAndConflictDetection(cwd: string, schemaFile: string) {
const schemaPath = getSchemaPath(schemaFile);
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -145,7 +145,7 @@ export function updateApiSchema(cwd: string, projectName: string, schemaName: st
}

export function updateApiWithMultiAuth(cwd: string, settings: any) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(settings.testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -196,7 +196,7 @@ export function updateApiWithMultiAuth(cwd: string, settings: any) {
}

export function apiUpdateToggleDataStore(cwd: string, settings: any) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(settings.testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand All @@ -216,7 +216,7 @@ export function apiUpdateToggleDataStore(cwd: string, settings: any) {
}

export function updateAPIWithResolutionStrategy(cwd: string, settings: any) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(settings.testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -252,7 +252,7 @@ export function updateAPIWithResolutionStrategy(cwd: string, settings: any) {

// Either settings.existingLambda or settings.isCrud is required
export function addRestApi(cwd: string, settings: any) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
if (!('existingLambda' in settings) && !('isCrud' in settings)) {
reject(new Error('Missing property in settings object in addRestApi()'));
} else {
Expand Down Expand Up @@ -317,13 +317,11 @@ export function addRestApi(cwd: string, settings: any) {
});
}

//add default api

const allAuthTypes = ['API key', 'Amazon Cognito User Pool', 'IAM', 'OpenID Connect'];

export function addApi(projectDir: string, settings?: any) {
let authTypesToSelectFrom = allAuthTypes.slice();
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
let chain = spawn(getCLIPath(), ['add', 'api'], { cwd: projectDir, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -450,7 +448,7 @@ function setupOIDC(chain: any, settings?: any) {
}

export function addApiWithCognitoUserPoolAuthTypeWhenAuthExists(projectDir: string) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd: projectDir, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendCarriageReturn()
Expand Down Expand Up @@ -479,7 +477,7 @@ export function addApiWithCognitoUserPoolAuthTypeWhenAuthExists(projectDir: stri
}

export function addRestContainerApi(projectDir: string) {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd: projectDir, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendKeyDown()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type Something
@model
@auth(rules: [{ allow: private, provider: iam }])
@key(name: "byTodo", fields: ["todoID"])
@key(name: "byTodo2", fields: ["todo2ID"])
@key(name: "byTodo3", fields: ["todo3ID"]) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
type Something @model @key(name: "byTodo", fields: ["todoID"]) @key(name: "byTodo2", fields: ["todo2ID"]) {
type Something
@model
@auth(rules: [{ allow: private, provider: iam }])
@key(name: "byTodo", fields: ["todoID"])
@key(name: "byTodo2", fields: ["todo2ID"]) {
id: ID!
todoID: ID!
todo2ID: ID!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
initJSProjectWithProfile,
deleteProject,
deleteProjectDir,
addApiWithSchema,
addFeatureFlag,
amplifyPush,
updateApiSchema,
amplifyPushUpdate,
addApiWithoutSchema,
updateApiWithMultiAuth
} from 'amplify-e2e-core';

describe('Schema iterative update - add new @models and @key', () => {
Expand All @@ -24,15 +25,17 @@ describe('Schema iterative update - add new @models and @key', () => {
await deleteProject(projectDir);
deleteProjectDir(projectDir);
});
it('should support adding a new @key to existing @model and adding multiple @modles ', async () => {
it('should support adding a new @key to existing @model and adding multiple @models with iam @auth enabled ', async () => {
const apiName = 'addkeyandmodel';

const initialSchema = path.join('iterative-push', 'add-one-key-multiple-models', 'initial-schema.graphql');
await addApiWithSchema(projectDir, initialSchema, { apiName, apiKeyExpirationDays: 7 });
await addApiWithoutSchema(projectDir, { apiName });
await updateApiWithMultiAuth(projectDir, {});
updateApiSchema(projectDir, apiName, initialSchema);
await amplifyPush(projectDir);

const finalSchema = path.join('iterative-push', 'add-one-key-multiple-models', 'final-schema.graphql');
await updateApiSchema(projectDir, apiName, finalSchema);
updateApiSchema(projectDir, apiName, finalSchema);
await amplifyPushUpdate(projectDir);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DeploymentOp, DeploymentStep } from '../iterative-deployment/deployment
import { DiffChanges, DiffableProject, getGQLDiff } from './utils';
import { DynamoDB, Template } from 'cloudform-types';
import { GSIChange, getGSIDiffs } from './gsi-diff-helpers';
import { GSIRecord, TemplateState, getStackParameters, getTableNames } from '../utils/amplify-resource-state-utils';
import { GSIRecord, TemplateState, getPreviousDeploymentRecord, getTableNames } from '../utils/amplify-resource-state-utils';
import { ROOT_APPSYNC_S3_KEY, hashDirectory } from '../upload-appsync-files';
import { addGSI, getGSIDetails, removeGSI } from './dynamodb-gsi-helpers';
import {
Expand Down Expand Up @@ -118,7 +118,7 @@ export class GraphQLResourceManager {

const tableNameMap = await getTableNames(this.cfnClient, this.templateState.getKeys(), this.resourceMeta.stackId);

const parameters = await getStackParameters(this.cfnClient, this.resourceMeta.stackId);
const { parameters, capabilities } = await getPreviousDeploymentRecord(this.cfnClient, this.resourceMeta.stackId);

const buildHash = await hashDirectory(this.backendApiProjectRoot);

Expand Down Expand Up @@ -148,6 +148,7 @@ export class GraphQLResourceManager {
parameters: { ...parameters, S3DeploymentRootKey: deploymentRootKey },
stackName: this.resourceMeta.stackId,
tableNames: tableNames,
capabilities,
// clientRequestToken: `${buildHash}-step-${stepNumber}`,
};

Expand All @@ -171,7 +172,7 @@ export class GraphQLResourceManager {
const cloudBuildDir = path.join(this.cloudBackendApiProjectRoot, 'build');
const stateFileDir = this.getStateFilesDirectory();

const parameters = await getStackParameters(this.cfnClient, this.resourceMeta.stackId);
const { parameters, capabilities } = await getPreviousDeploymentRecord(this.cfnClient, this.resourceMeta.stackId);
const buildHash = await hashDirectory(this.backendApiProjectRoot);

const stepNumber = 'initial-stack';
Expand All @@ -184,6 +185,7 @@ export class GraphQLResourceManager {
stackTemplatePathOrUrl: `${deploymentRootKey}/cloudformation-template.json`,
parameters: { ...parameters, S3DeploymentRootKey: deploymentRootKey },
stackName: this.resourceMeta.stackId,
capabilities,
tableNames: [],
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DeploymentMachineStep,
StateMachineHelperFunctions,
createDeploymentMachine,
StateMachineError
} from './state-machine';
import { IStackProgressPrinter, StackEventMonitor } from './stack-event-monitor';
import { getBucketKey, getHttpUrl } from './helpers';
Expand All @@ -24,6 +25,18 @@ interface DeploymentManagerOptions {
userAgent?: string;
}

export class DeploymentError extends Error {
constructor(errors: StateMachineError[]) {
super('There was an error while deploying changes.');
this.name = `DeploymentError`;
const stackTrace = [];
for (const err of errors) {
stackTrace.push(`Index: ${err.currentIndex} State: ${err.stateValue}\n${err.error.stack}`);
}
this.stack = JSON.stringify(stackTrace);
}
}

export type DeploymentOp = Omit<DeploymentMachineOp, 'region' | 'stackTemplatePath' | 'stackTemplateUrl'> & {
stackTemplatePathOrUrl: string;
};
Expand Down Expand Up @@ -133,8 +146,7 @@ export class DeploymentManager {
return resolve();
case 'rolledBack':
case 'failed':
return reject(new Error('Deployment failed'));
break;
return reject(new DeploymentError(state.context.errors));
default:
// intentionally left blank as we don't care about intermediate states
}
Expand Down Expand Up @@ -209,7 +221,7 @@ export class DeploymentManager {
await this.s3Client.headObject({ Bucket: this.deploymentBucket, Key: bucketKey }).promise();
return true;
} catch (e) {
if (e.ccode === 'NotFound') {
if (e.code === 'NotFound') {
throw new Error(`The cloudformation template ${templatePath} was not found in deployment bucket ${this.deploymentBucket}`);
}
throw e;
Expand All @@ -220,10 +232,8 @@ export class DeploymentManager {
assert(tableName, 'table name should be passed');

const dbClient = new aws.DynamoDB({ region });

const response = await dbClient.describeTable({ TableName: tableName }).promise();
const gsis = response.Table?.GlobalSecondaryIndexes;

return gsis ? gsis.every(idx => idx.IndexStatus === 'ACTIVE') : true;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import * as path from 'path';

import { DeployMachineContext, DeploymentMachineOp } from './state-machine';

export const collectError = (context: DeployMachineContext, err: any, meta: any) => {
return {
...context,
errors: [...(context.errors ? context.errors : []), { error: err.data, stateValue: meta.state.value, currentIndex: context.currentIndex }]
}
}

export const isRollbackComplete = (context: DeployMachineContext) => {
return context.currentIndex < 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
getRollbackOperationHandler,
isDeploymentComplete,
isRollbackComplete,
collectError,
} from './helpers';

import { send } from 'xstate/lib/actions';

export type DeploymentMachineOp = {
Expand All @@ -31,6 +31,7 @@ export type DeployMachineContext = {
region: string;
stacks: DeploymentMachineStep[];
currentIndex: number;
errors?: StateMachineError[];
};

export type DeploymentMachineEvents = 'IDLE' | 'DEPLOY' | 'ROLLBACK' | 'INDEX' | 'DONE' | 'NEXT';
Expand All @@ -44,6 +45,12 @@ export type DeploymentMachineState = State<
context: DeployMachineContext;
}
>;

export type StateMachineError = {
error: Error;
stateValue: number;
currentIndex: number;
}
export interface DeployMachineSchema {
states: {
idle: {};
Expand Down Expand Up @@ -122,6 +129,7 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
},
onError: {
target: '#rollback',
actions: assign(collectError),
},
},
},
Expand All @@ -134,6 +142,7 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
},
onError: {
target: '#rollback',
actions: assign(collectError),
},
},
activities: ['deployPoll'],
Expand All @@ -146,6 +155,9 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
target: 'triggerDeploy',
actions: send('NEXT'),
},
onError: {
actions: assign(collectError),
},
},
},
},
Expand Down Expand Up @@ -178,6 +190,7 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
},
onError: {
target: '#failed',
actions: assign(collectError),
},
},
},
Expand All @@ -190,6 +203,7 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
},
onError: {
target: '#failed',
actions: assign(collectError),
},
},
activities: ['rollbackPoll'],
Expand All @@ -202,6 +216,9 @@ export function createDeploymentMachine(initialContext: DeployMachineContext, he
target: 'triggerRollback',
actions: send('NEXT'),
},
onError: {
actions: assign(collectError),
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { Template } from 'cloudform-types';
import { GlobalSecondaryIndex, AttributeDefinition } from 'cloudform-types/types/dynamoDb/table';
import { CloudFormation } from 'aws-sdk';
import { Capabilities } from 'aws-sdk/clients/cloudformation';
import _ from 'lodash';
import { JSONUtilities } from 'amplify-cli-core';

export interface GSIRecord {
attributeDefinition: AttributeDefinition[];
gsi: GlobalSecondaryIndex;
}
/**
* Use previously deployed variables
*/
export interface DeploymentRecord {
parameters?: Record<string, string>;
capabilities?: Capabilities;
}

export const getStackParameters = async (cfnClient: CloudFormation, StackId: string): Promise<any> => {
export const getPreviousDeploymentRecord = async (cfnClient: CloudFormation, stackId: string): Promise<DeploymentRecord> => {
let depRecord: DeploymentRecord = {};
const apiStackInfo = await cfnClient
.describeStacks({
StackName: StackId,
StackName: stackId,
})
.promise();
return apiStackInfo.Stacks[0].Parameters.reduce((acc, param) => {
depRecord.parameters = apiStackInfo.Stacks[0].Parameters.reduce((acc, param) => {
acc[param.ParameterKey] = param.ParameterValue;
return acc;
}, {});
}, {}) as Record<string, string>;
depRecord.capabilities = apiStackInfo.Stacks[0].Capabilities;
return depRecord;
};

export const getTableNames = async (cfnClient: CloudFormation, tables: string[], StackId: string): Promise<Map<string, string>> => {
Expand Down

0 comments on commit 39dfd27

Please sign in to comment.