From b4669038ef80a67a9ae7dfd710682d86af2d908a Mon Sep 17 00:00:00 2001 From: Jimmy Gaussen Date: Fri, 20 Dec 2024 16:45:39 +0100 Subject: [PATCH 1/2] feat(codebuild): fleet proxy and vpc props --- .../aws-cdk-lib/aws-codebuild/lib/fleet.ts | 156 ++++++++++- .../aws-codebuild/test/fleet.test.ts | 260 ++++++++++++++++++ 2 files changed, 415 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts b/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts index 8020a86988c6b..82dc33a5fb129 100644 --- a/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts +++ b/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts @@ -1,8 +1,87 @@ import { Construct } from 'constructs'; +import * as ec2 from '../../aws-ec2'; +import { Arn, ArnFormat, IResource, Resource, Token } from '../../core'; import { CfnFleet } from './codebuild.generated'; import { ComputeType } from './compute-type'; import { EnvironmentType } from './environment-type'; -import { Arn, ArnFormat, IResource, Resource, Token } from '../../core'; + +/** + * FleetProxyConfiguration helper class + */ +export class FleetProxyConfiguration { + private readonly _rules: CfnFleet.FleetProxyRuleProperty[] = []; + + /** + * FleetProxyConfiguration helper constructor. + * @param defaultBehavior The default behavior for the fleet proxy configuration. + */ + constructor(public readonly defaultBehavior: FleetProxyDefaultBehavior) { + } + + /** + * The proxy configuration for the fleet. + */ + public get configuration(): CfnFleet.ProxyConfigurationProperty { + return { + defaultBehavior: this.defaultBehavior, + orderedProxyRules: this._rules, + }; + } + + /** + * Important: The order of rules is significant. The first rule that matches the traffic is applied. + * + * @param effect Whether the rule allows or denies traffic + * @param ipAddresses IPv4 and IPv6 addresses in CIDR notation + * @returns the current FleetProxyConfiguration to allow method chaining + */ + public addIpRule(effect: FleetProxyRuleEffect, ...ipAddresses: string[]): FleetProxyConfiguration { + this._rules.push({ + effect, + entities: ipAddresses, + type: 'IP', + }); + + return this; + } + + /** + * Important: The order of rules is significant. The first rule that matches the traffic is applied. + * + * @param effect Whether the rule allows or denies traffic + * @param domains Domain names + * @returns the current FleetProxyConfiguration to allow method chaining + */ + public addDomainRule(effect: FleetProxyRuleEffect, ...domains: string[]): FleetProxyConfiguration { + this._rules.push({ + effect, + entities: domains, + type: 'DOMAIN', + }); + + return this; + } +} + +/** + * TODO + */ +export interface FleetVpcConfiguration { + /** + * TODO + */ + readonly vpc: ec2.IVpc; + + /** + * TODO + */ + readonly subnets: ec2.ISubnet[]; + + /** + * TODO + */ + readonly securityGroups: ec2.ISecurityGroup[]; +} /** * Construction properties of a CodeBuild {@link Fleet}. @@ -35,6 +114,16 @@ export interface FleetProps { * made available to projects using this fleet */ readonly environmentType: EnvironmentType; + + /** + * TODO + */ + readonly proxyConfiguration?: FleetProxyConfiguration; + + /** + * TODO + */ + readonly vpcConfiguration?: FleetVpcConfiguration; } /** @@ -65,6 +154,16 @@ export interface IFleet extends IResource { * made available to projects using this fleet */ readonly environmentType: EnvironmentType; + + /** + * TODO + */ + readonly proxyConfiguration?: FleetProxyConfiguration; + + /** + * TODO + */ + readonly vpcConfiguration?: FleetVpcConfiguration; } /** @@ -138,13 +237,40 @@ export class Fleet extends Resource implements IFleet { throw new Error('baseCapacity must be greater than or equal to 1'); } + if (props.proxyConfiguration) { + if (![EnvironmentType.LINUX_CONTAINER, EnvironmentType.LINUX_GPU_CONTAINER].includes(props.environmentType)) { + throw new Error('proxyConfiguration can only be used if environmentType is "LINUX_CONTAINER" or "LINUX_GPU_CONTAINER"'); + } + + if (props.vpcConfiguration) { + throw new Error('proxyConfiguration and vpcConfiguration cannot be used concurrently'); + } + } + super(scope, id, { physicalName: props.fleetName }); + let fleetVpcConfig: CfnFleet.VpcConfigProperty | undefined; + if (props.vpcConfiguration) { + const { vpc, subnets, securityGroups } = props.vpcConfiguration; + if (!Token.isUnresolved(vpc.stack.account) && !Token.isUnresolved(this.stack.account) && + vpc.stack.account !== this.stack.account) { + throw new Error('VPC must be in the same account as its associated fleet'); + } + + fleetVpcConfig = { + vpcId: vpc.vpcId, + subnets: subnets.map(({ subnetId }) => subnetId), + securityGroupIds: securityGroups.map(({ securityGroupId }) => securityGroupId), + }; + } + const resource = new CfnFleet(this, 'Resource', { name: props.fleetName, baseCapacity: props.baseCapacity, computeType: props.computeType, environmentType: props.environmentType, + fleetProxyConfiguration: props.proxyConfiguration?.configuration, + fleetVpcConfig, }); this.fleetArn = this.getResourceArnAttribute(resource.attrArn, { @@ -204,3 +330,31 @@ export enum FleetComputeType { **/ X2_LARGE = ComputeType.X2_LARGE, } + +/** + * TODO + */ +export enum FleetProxyDefaultBehavior { + /** + * TODO + */ + ALLOW_ALL = 'ALLOW_ALL', + /** + * TODO + */ + DENY_ALL = 'DENY_ALL', +} + +/** + * TODO + */ +export enum FleetProxyRuleEffect { + /** + * TODO + */ + ALLOW = 'ALLOW', + /** + * TODO + */ + DENY = 'DENY', +} diff --git a/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts b/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts index d538541e44a7f..fee27b5be7f66 100644 --- a/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts +++ b/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts @@ -1,4 +1,5 @@ import { Match, Template } from '../../assertions'; +import * as ec2 from '../../aws-ec2'; import * as cdk from '../../core'; import * as codebuild from '../lib'; @@ -130,3 +131,262 @@ test('throws if trying to retrieve properties from an imported Fleet', () => { return fleet.environmentType; }).toThrow(/Cannot retrieve environmentType property from an imported Fleet/); }); + +describe('fleet proxy configuration', () => { + test('can specify a basic proxy configuration', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const proxyConfiguration = + new codebuild.FleetProxyConfiguration(codebuild.FleetProxyDefaultBehavior.DENY_ALL); + + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + proxyConfiguration, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetProxyConfiguration: { + DefaultBehavior: 'DENY_ALL', + }, + }); + }); + test('can add multiple rules', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const proxyConfiguration = + new codebuild.FleetProxyConfiguration(codebuild.FleetProxyDefaultBehavior.DENY_ALL) + .addIpRule(codebuild.FleetProxyRuleEffect.ALLOW, '1.2.3.4', '2.3.4.5') + .addDomainRule(codebuild.FleetProxyRuleEffect.DENY, 'example.com', 'example.org'); + proxyConfiguration + .addIpRule(codebuild.FleetProxyRuleEffect.DENY, '2001:0db8:85a3:0000:0000:abcd:0001:2345'); + + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + proxyConfiguration, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetProxyConfiguration: { + DefaultBehavior: 'DENY_ALL', + OrderedProxyRules: [ + { + Effect: 'ALLOW', + Entities: ['1.2.3.4', '2.3.4.5'], + Type: 'IP', + }, + { + Effect: 'DENY', + Entities: ['example.com', 'example.org'], + Type: 'DOMAIN', + }, + { + Effect: 'DENY', + Entities: ['2001:0db8:85a3:0000:0000:abcd:0001:2345'], + Type: 'IP', + }, + ], + }, + }); + }); + + test('throws if proxyConfiguration is used with environmentType different than LINUX_CONTAINER', () => { + // GIVEN + const stack = new cdk.Stack(); + + const proxyConfiguration = + new codebuild.FleetProxyConfiguration(codebuild.FleetProxyDefaultBehavior.DENY_ALL); + + // THEN + expect(() => { + new codebuild.Fleet(stack, 'Fleet', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.WINDOWS_SERVER_2019_CONTAINER, + baseCapacity: 1, + proxyConfiguration, + }); + }).toThrow(/proxyConfiguration can only be used if environmentType is "LINUX_CONTAINER" or "LINUX_GPU_CONTAINER"/); + }); + + test('throws if proxyConfiguration is used with VPC configuration', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); + + // THEN + expect(() => { + new codebuild.Fleet(stack, 'Fleet', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + proxyConfiguration: new codebuild.FleetProxyConfiguration(codebuild.FleetProxyDefaultBehavior.DENY_ALL), + vpcConfiguration: { + vpc, + subnets: vpc.privateSubnets, + securityGroups: [securityGroup], + }, + }); + }).toThrow(/proxyConfiguration and vpcConfiguration cannot be used concurrently/); + }); +}); + +describe('fleet VPC configuration', () => { + test('can specify a basic VPC configuration', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); + + // WHEN + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc, + subnets: vpc.privateSubnets, + securityGroups: [securityGroup], + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetVpcConfig: { + VpcId: { Ref: 'Vpc8378EB38' }, + Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A' }, { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }], + SecurityGroupIds: [{ 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }], + }, + }); + }); + + test('can use an imported VPC and security group', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = ec2.Vpc.fromVpcAttributes(stack, 'Vpc', { + vpcId: 'vpc-12345', + availabilityZones: ['az1'], + }); + const securityGroup = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-12345'); + + // WHEN + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc, + subnets: vpc.privateSubnets, + securityGroups: [securityGroup], + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetVpcConfig: { + VpcId: 'vpc-12345', + }, + }); + }); + + test('throws if VPC is in a different account than the fleet', () => { + // GIVEN + const [accountA, accountB] = ['123456789012', '012345678901']; + + const app = new cdk.App(); + const stackForAccountA = new cdk.Stack(app, 'StackA', { env: { account: accountA } }); + const stackForAccountB = new cdk.Stack(app, 'StackB', { env: { account: accountB } }); + const stackForUnresolvedAccount = new cdk.Stack(app, 'StackUnresolved'); + + const createdVpcForAccountB = new ec2.Vpc(stackForAccountB, 'CreatedVpcStackB'); + const importedVpcForAccountB = ec2.Vpc.fromVpcAttributes(stackForAccountB, 'ImportedVpcStackB', { + vpcId: 'vpc-12345', + availabilityZones: ['az1'], + }); + const securityGroup = new ec2.SecurityGroup(stackForAccountB, 'SecurityGroup', { + vpc: createdVpcForAccountB, + }); + + const vpcForUnresolvedAccount = new ec2.Vpc(stackForUnresolvedAccount, 'CreatedVpcStackUnresolved'); + + // THEN + expect(() => { + new codebuild.Fleet(stackForAccountA, 'FleetWithCreatedCrossAccountVpc', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc: createdVpcForAccountB, + securityGroups: [securityGroup], + subnets: createdVpcForAccountB.privateSubnets, + }, + }); + }).toThrow(/VPC must be in the same account as its associated fleet/); + + expect(() => { + new codebuild.Fleet(stackForAccountA, 'FleetWithImportedCrossAccountVpc', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc: importedVpcForAccountB, + securityGroups: [securityGroup], + subnets: importedVpcForAccountB.privateSubnets, + }, + }); + }).toThrow(/VPC must be in the same account as its associated fleet/); + + // Do not throw if both accounts aren't resolved + expect(() => { + new codebuild.Fleet(stackForAccountA, 'FleetWithUnresolvedAccountVpc', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc: vpcForUnresolvedAccount, + securityGroups: [securityGroup], + subnets: vpcForUnresolvedAccount.privateSubnets, + }, + }); + }).not.toThrow(); + expect(() => { + new codebuild.Fleet(stackForUnresolvedAccount, 'FleetInUnresolvedAccount', { + fleetName: 'MyFleet', + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + vpcConfiguration: { + vpc: createdVpcForAccountB, + securityGroups: [securityGroup], + subnets: createdVpcForAccountB.privateSubnets, + }, + }); + }).not.toThrow(); + }); +}); From 6d3ff3ee3f9a1ce31166fe89a0295c6fc1a80b15 Mon Sep 17 00:00:00 2001 From: Jimmy Gaussen Date: Sat, 21 Dec 2024 14:34:49 +0100 Subject: [PATCH 2/2] feat: fleet serviceRole --- .../aws-cdk-lib/aws-codebuild/lib/fleet.ts | 87 ++++++++++++++++--- .../aws-codebuild/test/fleet.test.ts | 48 ++++++++++ 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts b/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts index 82dc33a5fb129..52edb2a2e13a0 100644 --- a/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts +++ b/packages/aws-cdk-lib/aws-codebuild/lib/fleet.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import * as ec2 from '../../aws-ec2'; -import { Arn, ArnFormat, IResource, Resource, Token } from '../../core'; +import * as iam from '../../aws-iam'; +import { Arn, ArnFormat, Aws, IResource, Lazy, PhysicalName, Resource, Token } from '../../core'; import { CfnFleet } from './codebuild.generated'; import { ComputeType } from './compute-type'; import { EnvironmentType } from './environment-type'; @@ -124,6 +125,11 @@ export interface FleetProps { * TODO */ readonly vpcConfiguration?: FleetVpcConfiguration; + + /** + * TODO + */ + readonly serviceRole?: iam.IRole; } /** @@ -154,16 +160,6 @@ export interface IFleet extends IResource { * made available to projects using this fleet */ readonly environmentType: EnvironmentType; - - /** - * TODO - */ - readonly proxyConfiguration?: FleetProxyConfiguration; - - /** - * TODO - */ - readonly vpcConfiguration?: FleetVpcConfiguration; } /** @@ -176,7 +172,7 @@ export interface IFleet extends IResource { * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/fleets.html */ -export class Fleet extends Resource implements IFleet { +export class Fleet extends Resource implements IFleet, iam.IGrantable { /** * Creates a Fleet construct that represents an external fleet. * @@ -223,6 +219,16 @@ export class Fleet extends Resource implements IFleet { */ public readonly environmentType: EnvironmentType; + /** + * The principal to grant permissions to. + */ + public readonly grantPrincipal: iam.IPrincipal; + + /** + * The service role associated with the fleet. + */ + private readonly serviceRole: iam.IRole; + constructor(scope: Construct, id: string, props: FleetProps) { if (props.fleetName && !Token.isUnresolved(props.fleetName)) { if (props.fleetName.length < 2) { @@ -249,6 +255,12 @@ export class Fleet extends Resource implements IFleet { super(scope, id, { physicalName: props.fleetName }); + this.serviceRole = props.serviceRole ?? new iam.Role(this, 'ServiceRole', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + roleName: PhysicalName.GENERATE_IF_NEEDED, + }); + this.grantPrincipal = this.serviceRole; + let fleetVpcConfig: CfnFleet.VpcConfigProperty | undefined; if (props.vpcConfiguration) { const { vpc, subnets, securityGroups } = props.vpcConfiguration; @@ -262,6 +274,8 @@ export class Fleet extends Resource implements IFleet { subnets: subnets.map(({ subnetId }) => subnetId), securityGroupIds: securityGroups.map(({ securityGroupId }) => securityGroupId), }; + + this.grantVpcPermissionsToServiceRole(props.vpcConfiguration); } const resource = new CfnFleet(this, 'Resource', { @@ -271,6 +285,7 @@ export class Fleet extends Resource implements IFleet { environmentType: props.environmentType, fleetProxyConfiguration: props.proxyConfiguration?.configuration, fleetVpcConfig, + fleetServiceRole: this.serviceRole.roleArn, }); this.fleetArn = this.getResourceArnAttribute(resource.attrArn, { @@ -283,6 +298,54 @@ export class Fleet extends Resource implements IFleet { this.computeType = props.computeType; this.environmentType = props.environmentType; } + + /** + * + * Generated following the recommendations from the CodeBuild documentation + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/auth-and-access-control-iam-identity-based-access-control.html#customer-managed-policies-example-permission-policy-fleet-service-role + * @param vpcConfiguration Fleet VPC configuration from props + */ + private grantVpcPermissionsToServiceRole({ vpc, subnets, securityGroups }: FleetVpcConfiguration) { + const subnetIds = subnets.map(({ subnetId }) => subnetId); + const securityGroupIds = securityGroups.map(({ securityGroupId }) => securityGroupId); + + this.serviceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ec2:CreateNetworkInterface'], + resources: [ + ...subnetIds.map(subnetId => + Arn.format({ service: 'ec2', resource: 'subnet', resourceName: subnetId }, this.stack), + ), + ...securityGroupIds.map(securityGroupId => + Arn.format({ service: 'ec2', resource: 'security-group', resourceName: securityGroupId }, this.stack), + ), + Arn.format({ service: 'ec2', resource: 'network-interface', resourceName: '*' }, this.stack), + ], + })); + this.serviceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'ec2:DescribeVpcs', + 'ec2:ModifyNetworkInterfaceAttribute', + 'ec2:DeleteNetworkInterface', + ], + resources: ['*'], + })); + this.serviceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ec2:CreateNetworkInterfacePermission'], + resources: [Arn.format({ service: 'ec2', resource: 'network-interface', resourceName: '*' }, this.stack)], + conditions: { + StringEquals: { + 'ec2:Subnet': subnetIds.map(subnetId => + Arn.format({ service: 'ec2', resource: 'subnet', resourceName: subnetId }, this.stack), + ), + }, + }, + })); + } } /** diff --git a/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts b/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts index fee27b5be7f66..25079b1a1835d 100644 --- a/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts +++ b/packages/aws-cdk-lib/aws-codebuild/test/fleet.test.ts @@ -1,5 +1,6 @@ import { Match, Template } from '../../assertions'; import * as ec2 from '../../aws-ec2'; +import * as iam from '../../aws-iam'; import * as cdk from '../../core'; import * as codebuild from '../lib'; @@ -132,6 +133,53 @@ test('throws if trying to retrieve properties from an imported Fleet', () => { }).toThrow(/Cannot retrieve environmentType property from an imported Fleet/); }); +describe('fleet service role', () => { + test('default service role is created if not specified', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetServiceRole: { 'Fn::GetAtt': ['FleetServiceRole02EA2190', 'Arn'] }, + }); + }); + + test('can specify a service role prop', () => { + // GIVEN + const stack = new cdk.Stack(); + const serviceRole = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + }); + + // WHEN + new codebuild.Fleet(stack, 'Fleet', { + computeType: codebuild.FleetComputeType.SMALL, + environmentType: codebuild.EnvironmentType.LINUX_CONTAINER, + baseCapacity: 1, + serviceRole, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Fleet', { + BaseCapacity: 1, + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentType: 'LINUX_CONTAINER', + FleetServiceRole: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] }, + }); + }); + +}); + describe('fleet proxy configuration', () => { test('can specify a basic proxy configuration', () => { // GIVEN