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

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

Merged
merged 4 commits into from
Jan 28, 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
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