Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5ade104
chore: initial commit
Siqi-Shan Oct 1, 2024
90019a0
chore: debugging
Siqi-Shan Oct 1, 2024
ae910c3
chore: debugging cleaning
Siqi-Shan Oct 1, 2024
123a480
chore: debugging
Siqi-Shan Oct 2, 2024
7f5730f
fix: revising cdk and auth
Siqi-Shan Oct 2, 2024
5fd253a
fix: tests except subscription
Siqi-Shan Oct 2, 2024
153cfaa
chore: userpool test except subscription
Siqi-Shan Oct 3, 2024
73b25d6
chore: debugging
Siqi-Shan Oct 4, 2024
1f18fd7
chore: debugging
Siqi-Shan Oct 4, 2024
1a03e65
fix: ttl duration unit conversion + e2e test
Siqi-Shan Oct 5, 2024
8536347
chore: revert back construct changes to another PR
Siqi-Shan Oct 5, 2024
3f72891
chore: initial cdk refactor
Siqi-Shan Oct 5, 2024
b5a22de
chore: cleanup
Siqi-Shan Oct 5, 2024
69ac8d2
refactor: major refactor to sql confogurable stack and config write
Siqi-Shan Oct 5, 2024
6e68ea0
refactor: configurable stack for prev tests
Siqi-Shan Oct 6, 2024
691f1f5
chore: naming + trimming
Siqi-Shan Oct 6, 2024
04c21ef
refactor: test setup + stack config
Siqi-Shan Oct 7, 2024
a409c75
chore: cleanup
Siqi-Shan Oct 7, 2024
17c7e60
fix: async cleanup fix
Siqi-Shan Oct 7, 2024
61e2404
Merge branch 'main' into gen2-migrate-pg-userpool-auth-e2e-test
Siqi-Shan Oct 7, 2024
d6645da
refactor: appsync client refactor
Siqi-Shan Oct 7, 2024
be1759e
refactor: appsync client helper
Siqi-Shan Oct 7, 2024
65ecb07
refactor: cognito helper + initial test
Siqi-Shan Oct 7, 2024
9d377ed
refactor: tester + test pattern extract
Siqi-Shan Oct 8, 2024
fab3911
refactor: auth tester generic function + crudl helper
Siqi-Shan Oct 9, 2024
d30adf8
refactor: add pg auto increment test from main
Siqi-Shan Oct 9, 2024
268467c
Merge branch 'main' into gen2-migrate-pg-userpool-auth-e2e-test
Siqi-Shan Oct 9, 2024
311a0a3
refactor: test conflict resolved + merge main
Siqi-Shan Oct 9, 2024
e98c99c
Merge branch 'main' into gen2-migrate-pg-userpool-auth-e2e-test
Siqi-Shan Oct 10, 2024
f4d541c
Merge branch 'main' into gen2-migrate-pg-userpool-auth-e2e-test
Siqi-Shan Oct 11, 2024
2bbcd68
chore: naming + schema change
Siqi-Shan Oct 15, 2024
51e2f97
Merge branch 'main' into gen2-migrate-pg-userpool-auth-e2e-test
Siqi-Shan Oct 15, 2024
5f16873
refactor: sql provider helper to support my directives and mysql
Siqi-Shan Oct 15, 2024
c9cbece
fix: postgres schema formatting
Siqi-Shan Oct 15, 2024
de91277
fix: correct escape when converting sql create statements
Siqi-Shan Oct 15, 2024
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
150 changes: 85 additions & 65 deletions codebuild_specs/e2e_workflow.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
#!/usr/bin/env node
import 'source-map-support/register';
import { App, Stack, CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib';
// @ts-ignore
import {
AmplifyGraphqlApi,
AmplifyGraphqlDefinition,
AuthorizationModes,
IAmplifyGraphqlDefinition,
SqlModelDataSourceDbConnectionConfig,
ModelDataSourceStrategySqlDbType,
PartialTranslationBehavior,
} from '@aws-amplify/graphql-api-construct';
import { AccountPrincipal, Effect, PolicyDocument, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { CfnUserPoolGroup, UserPool, UserPoolClient, UserPoolTriggers } from 'aws-cdk-lib/aws-cognito';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

// #region Utilities

enum AUTH_MODE {
API_KEY = 'API_KEY',
AWS_IAM = 'AWS_IAM',
AMAZON_COGNITO_USER_POOLS = 'AMAZON_COGNITO_USER_POOLS',
OPENID_CONNECT = 'OPENID_CONNECT',
AWS_LAMBDA = 'AWS_LAMBDA',
}

enum SCHEMA {
DEFAULT = /* GraphQL */ `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we represent this as an enum vs a string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both options are viable here. My thought originally was that there could be multiple existing schemas available in the stack which could be used without providing schema through stack-config but more like an option. But that could be an overdesign here, as almost every test's schema is different, and it's impossible to store all those schemas in the CDK file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe a similar question is why enum versus an object with a string field default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to re-define these Auth modes and set a default for schema? Instead we could just fail if either of them is not defined in the test using it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could keep the Auth mode to sync with the stack config interface that is using the AUTH_TYPE from AppSync, while schema default could be discarded to avoid overdesign, since schema is already required when defining the stack config interface.

type Todo @model @refersTo(name: "todos") {
id: ID! @primaryKey
description: String!
}
`,
}

interface DBDetails {
dbConfig: {
endpoint: string;
port: number;
dbName: string;
dbType: ModelDataSourceStrategySqlDbType;
strategyName: string;
vpcConfig: {
vpcId: string;
securityGroupIds: string[];
subnetAvailabilityZones: [
{
subnetId: string;
availabilityZone: string;
},
];
};
};
dbConnectionConfig: SqlModelDataSourceDbConnectionConfig;
}

interface StackConfig {
/**
* The AppSync GraphQL schema, provided as string for AmplifyGraphqlApi Construct definition.
*/
schema: string;

/**
* The AuthorizationMode type for AmplifyGraphqlApi Construct.
*/
authMode?: AUTH_MODE;

/**
* If true, disable Cognito User Pool/Auth resources creation and only use API Key auth in sandbox mode.
*/
useSandbox?: boolean;

/**
* Cognito User Pool groups to create when provisioning the User Pool.
*
* **NOTE**
* Provide at least two group names for setup and testing purposes.
*/
userGroups?: string[];

/**
* The OIDC options/config when using OIDC AuthorizationMode for AmplifyGraphqlApi Construct.
*
* @property {Record<string, string>} [triggers] - UserPoolTriggers for Cognito User Pool when provisioning the User Pool as OIDC provider.
* - key: trigger name e.g. 'preTokenGeneration'
* - value: the lambda function code inlined as a string
*
* **NOTE**
* - Only applicable when AuthorizationMode is set to OIDC.
* - Currently only supports Cognito User Pools as the simulated OIDC provider for E2E test.
* - Currently only supports JavaScript as the lambda function code, with Node.js runtime version 18.x.
* - Inline code needs to export the handler function as `handler` as `index.handler` would be used as the handler path.
*/
oidcOptions?: {
triggers: Record<string, string>;
};
}

const createApiDefinition = (): IAmplifyGraphqlDefinition => {
const schema = stackConfig.schema ?? SCHEMA.DEFAULT;

return AmplifyGraphqlDefinition.fromString(schema, {
name: dbDetails.dbConfig.strategyName,
dbType: dbDetails.dbConfig.dbType,
vpcConfiguration: {
vpcId: dbDetails.dbConfig.vpcConfig.vpcId,
securityGroupIds: dbDetails.dbConfig.vpcConfig.securityGroupIds,
subnetAvailabilityZoneConfig: dbDetails.dbConfig.vpcConfig.subnetAvailabilityZones,
},
dbConnectionConfig: {
...dbDetails.dbConnectionConfig,
},
sqlLambdaProvisionedConcurrencyConfig: {
provisionedConcurrentExecutions: 2,
},
});
};

const createAuthorizationModes = (): AuthorizationModes => {
const auth = stackConfig.authMode ?? AUTH_MODE.API_KEY;
let authorizationModes: AuthorizationModes;

switch (auth) {
case AUTH_MODE.API_KEY: {
authorizationModes = {
defaultAuthorizationMode: 'API_KEY',
apiKeyConfig: { expires: Duration.days(7) },
};

break;
}
case AUTH_MODE.AMAZON_COGNITO_USER_POOLS: {
const { userPool, userPoolClient } = createUserPool(dbDetails.dbConfig.dbName);

authorizationModes = {
defaultAuthorizationMode: 'AMAZON_COGNITO_USER_POOLS',
userPoolConfig: {
userPool,
},
apiKeyConfig: { expires: Duration.days(2) },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the API key to be valid for 2 days? Especially since we teardown these apps post test run, I would recommend setting it to 1 hr to begin with.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 75 of file authorization-modes.ts in amplify-graphql-api-construct enforces the Duration would be converted to days with following definition: apiKeyExpirationDays: authMode.expires.toDays()

Setting expiration to hours would cause CDK deployment to fail with error message:

  • "Error: '1 hours' cannot be converted into a whole number of days."

We could adjust the construct time conversion if needed to give test a more tight time boundary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could adjust the construct time conversion if needed to give test a more tight time boundary.

That might be the right way to go forward here.

};

break;
}
case AUTH_MODE.OPENID_CONNECT: {
const { userPool, userPoolClient } = createUserPool(dbDetails.dbConfig.dbName, stackConfig.oidcOptions?.triggers);

authorizationModes = {
defaultAuthorizationMode: 'OPENID_CONNECT',
oidcConfig: {
oidcProviderName: 'awscognitouserpool',
oidcIssuerUrl: `https://cognito-idp.${stack.region}.amazonaws.com/${userPool.userPoolId}`,
clientId: userPoolClient.userPoolClientId,
tokenExpiryFromAuth: Duration.hours(1),
tokenExpiryFromIssue: Duration.hours(1),
},
};

break;
}
default: {
throw new Error(`Unsupported auth mode: ${stackConfig.authMode}`);
}
}

return authorizationModes;
};

const createTranslationBehavior = (): PartialTranslationBehavior => {
const translationBehavior: PartialTranslationBehavior = {
sandboxModeEnabled: stackConfig.useSandbox ?? false,
};
return translationBehavior;
};

const createUserPool = (prefix: string, triggers?: Record<string, string>): { userPool: UserPool; userPoolClient: UserPoolClient } => {
const userPool = new UserPool(stack, `${prefix}UserPool`, {
signInAliases: {
username: true,
email: false,
},
selfSignUpEnabled: true,
autoVerify: { email: true },
standardAttributes: {
email: {
required: true,
mutable: false,
},
},
lambdaTriggers: triggers ? createUserPoolTriggers(triggers) : {},
});
userPool.applyRemovalPolicy(RemovalPolicy.DESTROY);

stackConfig.userGroups?.forEach((group) => {
new CfnUserPoolGroup(userPool, `Group${group}`, {
userPoolId: userPool.userPoolId,
groupName: group,
});
});

const userPoolClient = userPool.addClient(`${prefix}UserPoolClient`, {
authFlows: {
userPassword: true,
userSrp: true,
},
});

new CfnOutput(stack, 'userPoolId', { value: userPool.userPoolId });
new CfnOutput(stack, 'webClientId', { value: userPoolClient.userPoolClientId });

return { userPool, userPoolClient };
};

const createUserPoolTriggers = (triggers: Record<string, string>): UserPoolTriggers => {
const userPoolTriggers: UserPoolTriggers = {};

Object.keys(triggers).forEach((triggerName) => {
userPoolTriggers[triggerName] = createLambdaFunction(triggerName, triggers[triggerName]);
});

return userPoolTriggers;
};

const createLambdaFunction = (name: string, code: string): Function => {
return new Function(stack, `${name}Lambda`, {
runtime: Runtime.NODEJS_18_X,
handler: 'index.handler',
code: Code.fromInline(code),
});
};

const createBasicRole = () => {
const basicRole = new Role(stack, 'BasicRole', {
assumedBy: new AccountPrincipal(stack.account),
path: '/',
inlinePolicies: {
root: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
resources: ['arn:aws:logs:*:*:*'],
effect: Effect.ALLOW,
}),
],
}),
},
});
basicRole.applyRemovalPolicy(RemovalPolicy.DESTROY);

basicRole.addToPolicy(
new PolicyStatement({
actions: ['appsync:GraphQL'],
resources: [`${api.resources.graphqlApi.arn}/*`],
effect: Effect.ALLOW,
}),
);

new CfnOutput(stack, 'BasicRoleArn', { value: basicRole.roleArn });
};

const createAdditionalCfnOutputs = () => {
switch (stackConfig.authMode) {
case AUTH_MODE.API_KEY: {
const {
resources: { functions },
} = api;
const sqlLambda = functions[`SQLFunction${dbDetails.dbConfig.strategyName}`];

new CfnOutput(stack, 'SQLFunctionName', { value: sqlLambda.functionName });
}
default: {
new CfnOutput(stack, 'GraphQLApiId', { value: api.resources.graphqlApi.apiId });
new CfnOutput(stack, 'GraphQLApiArn', { value: api.resources.graphqlApi.arn });
new CfnOutput(stack, 'region', { value: stack.region });
}
}
};

// #endregion Utilities

// #region CDK App

// eslint-disable-next-line @typescript-eslint/no-var-requires
const dbDetails: DBDetails = require('../db-details.json');
const stackConfig: StackConfig = require('../stack-config.json');

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');

const app = new App();
const stack = new Stack(app, packageJson.name.replace(/_/g, '-'), {
env: { region: process.env.CLI_REGION || 'us-west-2' },
});

const definition = createApiDefinition();
const authorizationModes = createAuthorizationModes();
const translationBehavior = createTranslationBehavior();

const api = new AmplifyGraphqlApi(stack, `SqlBoundApi`, {
apiName: `${dbDetails.dbConfig.dbType}${Date.now()}`,
definition,
authorizationModes,
translationBehavior,
});

createBasicRole();
createAdditionalCfnOutputs();

// #endregion CDK App

This file was deleted.

Loading
Loading