Skip to content

Commit

Permalink
feat: migrate to CDK for integration test suite (#882)
Browse files Browse the repository at this point in the history
* test: add infra setup

* chore: add API handler

* chore: use HTTP API

* fix: integration tests using CDK

* setup action

* fix: remove extra assume role

* test: run integration tests in CI

* fix: disable lint on infra dir

* fix: allow creation of OIDC token

* fix: sub claim

* fix: arn

* fix: grant describe stacks

* fix: limit stack ID

* fix: allow assume role

* fix: invert assume role trust relationship
  • Loading branch information
jamesmbourne authored May 5, 2023
1 parent fac8d10 commit 17cb227
Show file tree
Hide file tree
Showing 18 changed files with 1,407 additions and 112 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"parserOptions": {
"project": "./tsconfig.json"
},
"ignorePatterns": ["infra/"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Node.js CI

on: [pull_request]

permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -22,3 +26,9 @@ jobs:
- run: npm run test
env:
CI: true
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- run: npm run test-it
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 18.16.0
8 changes: 8 additions & 0 deletions infra/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions infra/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
14 changes: 14 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
21 changes: 21 additions & 0 deletions infra/bin/infra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { AWSv4AxiosInfraStack } from "../lib/infra-stack";

const app = new cdk.App();
new AWSv4AxiosInfraStack(app, "AWSv4AxiosInfraStack", {
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: {
// account: process.env.CDK_DEFAULT_ACCOUNT,
// region: process.env.CDK_DEFAULT_REGION,
// },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
53 changes: 53 additions & 0 deletions infra/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"app": "npx ts-node --prefer-ts-exts bin/infra.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true
}
}
8 changes: 8 additions & 0 deletions infra/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
};
8 changes: 8 additions & 0 deletions infra/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

export const handler: APIGatewayProxyHandlerV2 = async (event, _context) => {
return {
body: JSON.stringify(event),
statusCode: 200,
};
};
142 changes: 142 additions & 0 deletions infra/lib/infra-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as cdk from "aws-cdk-lib";
import * as apigatewayv2 from "@aws-cdk/aws-apigatewayv2-alpha";
import { HttpIamAuthorizer } from "@aws-cdk/aws-apigatewayv2-authorizers-alpha";
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class AWSv4AxiosInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
const routeArn = ({
apiId,
stage,
httpMethod,
path,
}: {
apiId: string;
stage?: string;
httpMethod: apigatewayv2.HttpMethod;
path?: string;
}): string => {
const iamHttpMethod =
httpMethod === apigatewayv2.HttpMethod.ANY ? "*" : httpMethod;

// When the user has provided a path with path variables, we replace the
// path variable and all that follows with a wildcard.
const iamPath = (path ?? "/").replace(/\{.*?\}.*/, "*");
const iamStage = stage ?? "*";

return `arn:${cdk.Aws.PARTITION}:execute-api:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:${apiId}/${iamStage}/${iamHttpMethod}${iamPath}`;
};

super(scope, id, props);

const apiHandler = new lambda.NodejsFunction(this, "ApiHandler", {
entry: "lib/api.ts",
handler: "handler",
});

const authorizer = new HttpIamAuthorizer();

const api = new apigatewayv2.HttpApi(this, "Api", {
defaultAuthorizer: authorizer,
defaultIntegration: new HttpLambdaIntegration(
"ApiHandlerIntegration",
apiHandler
),
});

if (!api.url) {
throw new Error("api.url is undefined");
}

// output URL as HttpApiUrl
new cdk.CfnOutput(this, "HttpApiUrl", {
value: api.url,
});

const clientRole = new iam.Role(this, "ClientRole", {
assumedBy: new iam.AccountRootPrincipal(),
});

// grant the client role access to the API
clientRole.addToPolicy(
new iam.PolicyStatement({
actions: ["execute-api:Invoke"],
resources: [
routeArn({
apiId: api.httpApiId,
stage: api.defaultStage?.stageName,
httpMethod: apigatewayv2.HttpMethod.ANY,
path: "/*",
}),
],
})
);

// output the client role ARN
new cdk.CfnOutput(this, "ClientRoleArn", {
value: clientRole.roleArn,
});

// create another role, assumable by the first
const assumedClientRole = new iam.Role(this, "AssumedClientRole", {
assumedBy: new iam.ArnPrincipal(clientRole.roleArn),
});

// set up an IAM role assumable by GitHub Actions using web identity federation
const githubActionsRole = new iam.Role(this, "GitHubActionsRole", {
assumedBy: new iam.WebIdentityPrincipal(
`arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`,
{
StringLike: {
"token.actions.githubusercontent.com:sub":
"repo:jamesmbourne/aws4-axios:*",
},
StringEquals: {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
},
}
),
// conditions
});

this.stackId;

// grant the GitHub Actions role access to CloudFormation describeStacks this stack
githubActionsRole.addToPolicy(
new iam.PolicyStatement({
actions: ["cloudformation:DescribeStacks"],
resources: [this.stackId],
})
);

clientRole.grantAssumeRole(githubActionsRole);

// output the GitHub Actions role ARN
new cdk.CfnOutput(this, "GitHubActionsRoleArn", {
value: githubActionsRole.roleArn,
});

// grant the assumed role access to the API
assumedClientRole.addToPolicy(
new iam.PolicyStatement({
actions: ["execute-api:Invoke"],
resources: [
routeArn({
apiId: api.httpApiId,
stage: api.defaultStage?.stageName,
httpMethod: apigatewayv2.HttpMethod.ANY,
path: "/*",
}),
],
})
);

// output the assumed client role ARN
new cdk.CfnOutput(this, "AssumedClientRoleArn", {
value: assumedClientRole.roleArn,
});
}
}
27 changes: 27 additions & 0 deletions infra/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "infra",
"version": "0.1.0",
"private": true,
"bin": {
"infra": "bin/infra.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.115",
"@types/node": "^20.0.0",
"aws-cdk": "2.78.0",
"ts-node": "^10.9.1"
},
"dependencies": {
"@aws-cdk/aws-apigatewayv2-authorizers-alpha": "^2.78.0-alpha.0",
"@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.78.0-alpha.0",
"aws-cdk-lib": "2.78.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
17 changes: 17 additions & 0 deletions infra/test/infra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// import * as cdk from 'aws-cdk-lib';
// import { Template } from 'aws-cdk-lib/assertions';
// import * as Infra from '../lib/infra-stack';

// example test. To run these tests, uncomment this file along with the
// example resource in lib/infra-stack.ts
test('SQS Queue Created', () => {
// const app = new cdk.App();
// // WHEN
// const stack = new Infra.InfraStack(app, 'MyTestStack');
// // THEN
// const template = Template.fromStack(stack);

// template.hasResourceProperties('AWS::SQS::Queue', {
// VisibilityTimeout: 300
// });
});
31 changes: 31 additions & 0 deletions infra/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": [
"es2020",
"dom"
],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"typeRoots": [
"./node_modules/@types"
]
},
"exclude": [
"node_modules",
"cdk.out"
]
}
Loading

0 comments on commit 17cb227

Please sign in to comment.