diff --git a/src/pipeline.ts b/src/pipeline.ts index 96952ad1..ea508e9d 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -218,6 +218,7 @@ export class GitHubWorkflow extends PipelineBase { this.addStackProps(stacks, 'environment', options?.gitHubEnvironment); this.addStackProps(stacks, 'capabilities', options?.stackCapabilities); this.addStackProps(stacks, 'settings', options?.jobSettings); + this.addStackProps(stacks, 'role', options?.gitHubActionsRoleArn); return stageDeployment; } @@ -370,6 +371,7 @@ export class GitHubWorkflow extends PipelineBase { const cdkoutDir = options.assemblyDir; const jobId = node.uniqueId; const assetId = assets[0].assetId; + const stageRoleArn = this.ghaStageRoleArn(); // check if asset is docker asset and if we have docker credentials const dockerLoginSteps: github.JobStep[] = []; @@ -419,7 +421,7 @@ export class GitHubWorkflow extends PipelineBase { name: 'Install', run: `npm install --no-save cdk-assets${installSuffix}`, }, - ...this.stepsToConfigureAws(this.useGitHubActionRole, { region: this.publishAssetsAuthRegion }), + ...this.stepsToConfigureAws(this.useGitHubActionRole, { region: this.publishAssetsAuthRegion, stageRoleArn: stageRoleArn }), ...dockerLoginSteps, publishStep, ], @@ -470,6 +472,8 @@ export class GitHubWorkflow extends PipelineBase { } const assumeRoleArn = stack.assumeRoleArn ? resolve(stack.assumeRoleArn) : undefined; + const stageRoleArn = this.ghaStageRoleArn(); + return { id: node.uniqueId, definition: { @@ -486,7 +490,7 @@ export class GitHubWorkflow extends PipelineBase { needs: this.renderDependencies(node), runsOn: this.runner.runsOn, steps: [ - ...this.stepsToConfigureAws(this.useGitHubActionRole, { region, assumeRoleArn }), + ...this.stepsToConfigureAws(this.useGitHubActionRole, { region, assumeRoleArn, stageRoleArn }), { id: 'Deploy', uses: 'aws-actions/aws-cloudformation-github-deploy@v1', @@ -639,7 +643,16 @@ export class GitHubWorkflow extends PipelineBase { }; } - private stepsToConfigureAws(openId: boolean, { region, assumeRoleArn }: { region: string; assumeRoleArn?: string }): github.JobStep[] { + private ghaStageRoleArn(): string { + const stageRoleArn = Object.entries(this.stackProperties)[0][1].role; + return stageRoleArn || this.gitHubActionRoleArn; + } + + private stepsToConfigureAws(openId: boolean, { + region, + assumeRoleArn, + stageRoleArn, + }: { region: string; assumeRoleArn?: string; stageRoleArn?: string }): github.JobStep[] { function getDeployRole(arn: string) { return arn.replace('cfn-exec', 'deploy'); } @@ -649,7 +662,7 @@ export class GitHubWorkflow extends PipelineBase { if (openId) { steps.push(awsCredentialStep('Authenticate Via OIDC Role', { region, - gitHubActionRoleArn: this.gitHubActionRoleArn, + gitHubActionRoleArn: stageRoleArn, })); if (assumeRoleArn) { diff --git a/src/stage-options.ts b/src/stage-options.ts index 27bb8805..ec92f023 100644 --- a/src/stage-options.ts +++ b/src/stage-options.ts @@ -37,6 +37,18 @@ export interface AddGitHubStageOptions extends AddStageOpts { * Currently the only valid setting is 'if'. */ readonly jobSettings?: JobSettings; + + /** + * A role that utilizes the GitHub OIDC Identity Provider in your AWS account. + * If supplied, this will be used instead of `awsCredentials`. + * + * You can create your own role in the console with the necessary trust policy + * to allow gitHub actions from your gitHub repository to assume the role, or + * you can utilize the `GitHubActionRole` construct to create a role for you. + * + * @default - GitHub repository secrets are used instead of OpenId Connect role. + */ + readonly gitHubActionsRoleArn?: string; } /** diff --git a/test/__snapshots__/stage-options.test.ts.snap b/test/__snapshots__/stage-options.test.ts.snap index 5ef5c667..874c6672 100644 --- a/test/__snapshots__/stage-options.test.ts.snap +++ b/test/__snapshots__/stage-options.test.ts.snap @@ -389,3 +389,100 @@ jobs: role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1 " `; + +exports[`role settings can specify role override settings at stage level 1`] = ` +"name: deploy +on: + push: + branches: + - main + workflow_dispatch: {} +jobs: + Build-Build: + name: Synthesize + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + needs: [] + env: {} + container: null + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install + run: yarn + - name: Build + run: yarn build + - name: Upload cdk.out + uses: actions/upload-artifact@v2.1.1 + with: + name: cdk.out + path: cdk.out + Assets-FileAsset1: + name: Publish Assets Assets-FileAsset1 + needs: + - Build-Build + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + outputs: + asset-hash: \${{ steps.Publish.outputs.asset-hash }} + steps: + - name: Download cdk.out + uses: actions/download-artifact@v2 + with: + name: cdk.out + path: stage.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via OIDC Role + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: us-west-2 + role-duration-seconds: 1800 + role-skip-session-tagging: true + role-to-assume: my-stage-role + - id: Publish + name: Publish Assets-FileAsset1 + run: /bin/bash ./cdk.out/publish-Assets-FileAsset1-step.sh + MyStack-MyStack-Deploy: + name: Deploy MyStack098574E7 + if: github.repository == 'github/repo' + permissions: + contents: read + id-token: write + needs: + - Build-Build + - Assets-FileAsset1 + runs-on: ubuntu-latest + steps: + - name: Authenticate Via OIDC Role + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: us-east-1 + role-duration-seconds: 1800 + role-skip-session-tagging: true + role-to-assume: my-stage-role + - name: Assume CDK Deploy Role + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: us-east-1 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ env.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ env.AWS_SECRET_ACCESS_KEY }} + aws-session-token: \${{ env.AWS_SESSION_TOKEN }} + role-to-assume: arn:aws:iam::111111111111:role/cdk-hnb659fds-deploy-role-111111111111-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStack-MyStack + template: https://cdk-hnb659fds-assets-111111111111-us-east-1.s3.us-east-1.amazonaws.com/\${{ + needs.Assets-FileAsset1.outputs.asset-hash }}.json + no-fail-on-empty-changeset: \\"1\\" + role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1 +" +`; diff --git a/test/stage-options.test.ts b/test/stage-options.test.ts index 4e248f81..a811a1aa 100644 --- a/test/stage-options.test.ts +++ b/test/stage-options.test.ts @@ -197,3 +197,64 @@ describe('job settings', () => { }); }); }); + +describe('role settings', () => { + test('can specify gha role', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + installCommands: ['yarn'], + commands: ['yarn build'], + }), + gitHubActionRoleArn: 'my-pipeline-role', + }); + + const stage = new Stage(app, 'MyStack', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + new Stack(stage, 'MyStack'); + + pipeline.addStageWithGitHubOptions(stage, { + jobSettings: { + if: 'github.repository == \'github/repo\'', + }, + }); + + app.synth(); + + expect(readFileSync(pipeline.workflowPath, 'utf-8')).toContain('if: github.repository == \'github/repo\'\n'); + }); + }); + + test('can specify role override settings at stage level', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + installCommands: ['yarn'], + commands: ['yarn build'], + }), + gitHubActionRoleArn: 'my-pipeline-role', + }); + + const stage = new Stage(app, 'MyStack', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + new Stack(stage, 'MyStack'); + + pipeline.addStageWithGitHubOptions(stage, { + jobSettings: { + if: 'github.repository == \'github/repo\'', + }, + gitHubActionsRoleArn: 'my-stage-role', + }); + + app.synth(); + + expect(readFileSync(pipeline.workflowPath, 'utf-8')).toMatchSnapshot(); + }); + }); +});