@@ -10,6 +10,7 @@ import {
1010 DescribeInstancesCommand ,
1111 type DescribeInstancesResult ,
1212 EC2Client ,
13+ RunInstancesCommand ,
1314 SpotAllocationStrategy ,
1415 TerminateInstancesCommand ,
1516} from '@aws-sdk/client-ec2' ;
@@ -964,6 +965,7 @@ interface RunnerConfig {
964965 onDemandFailoverOnError ?: string [ ] ;
965966 scaleErrors : string [ ] ;
966967 source : LambdaRunnerSource ;
968+ useDedicatedHost ?: boolean ;
967969}
968970
969971function createRunnerConfig ( runnerConfig : RunnerConfig ) : RunnerInputParameters {
@@ -985,6 +987,7 @@ function createRunnerConfig(runnerConfig: RunnerConfig): RunnerInputParameters {
985987 onDemandFailoverOnError : runnerConfig . onDemandFailoverOnError ,
986988 scaleErrors : runnerConfig . scaleErrors ,
987989 source : runnerConfig . source ,
990+ useDedicatedHost : runnerConfig . useDedicatedHost ,
988991 } ;
989992}
990993
@@ -1073,3 +1076,211 @@ function expectedCreateFleetRequest(expectedValues: ExpectedFleetRequestValues):
10731076
10741077 return request ;
10751078}
1079+
1080+ describe ( 'create runner with useDedicatedHost' , ( ) => {
1081+ const dedicatedHostRunnerConfig : RunnerConfig = {
1082+ allocationStrategy : SpotAllocationStrategy . CAPACITY_OPTIMIZED ,
1083+ capacityType : 'on-demand' ,
1084+ type : 'Org' ,
1085+ scaleErrors : [ ] ,
1086+ useDedicatedHost : true ,
1087+ } ;
1088+
1089+ beforeEach ( ( ) => {
1090+ vi . clearAllMocks ( ) ;
1091+ mockEC2Client . reset ( ) ;
1092+ mockSSMClient . reset ( ) ;
1093+
1094+ mockEC2Client . on ( RunInstancesCommand ) . resolves ( {
1095+ Instances : [ { InstanceId : 'i-dedicated-1' } ] ,
1096+ } ) ;
1097+ mockSSMClient . on ( GetParameterCommand ) . resolves ( { } ) ;
1098+ } ) ;
1099+
1100+ it ( 'uses RunInstances instead of CreateFleet when useDedicatedHost is true' , async ( ) => {
1101+ const result = await createRunner ( createRunnerConfig ( dedicatedHostRunnerConfig ) ) ;
1102+
1103+ expect ( result ) . toEqual ( [ 'i-dedicated-1' ] ) ;
1104+ expect ( mockEC2Client ) . toHaveReceivedCommand ( RunInstancesCommand ) ;
1105+ expect ( mockEC2Client ) . not . toHaveReceivedCommand ( CreateFleetCommand ) ;
1106+ } ) ;
1107+
1108+ it ( 'uses CreateFleet when useDedicatedHost is false' , async ( ) => {
1109+ mockEC2Client . on ( CreateFleetCommand ) . resolves ( { Instances : [ { InstanceIds : [ 'i-fleet-1' ] } ] } ) ;
1110+
1111+ const result = await createRunner ( createRunnerConfig ( {
1112+ ...dedicatedHostRunnerConfig ,
1113+ useDedicatedHost : false ,
1114+ } ) ) ;
1115+
1116+ expect ( result ) . toEqual ( [ 'i-fleet-1' ] ) ;
1117+ expect ( mockEC2Client ) . toHaveReceivedCommand ( CreateFleetCommand ) ;
1118+ expect ( mockEC2Client ) . not . toHaveReceivedCommand ( RunInstancesCommand ) ;
1119+ } ) ;
1120+
1121+ it ( 'uses CreateFleet when useDedicatedHost is undefined' , async ( ) => {
1122+ mockEC2Client . on ( CreateFleetCommand ) . resolves ( { Instances : [ { InstanceIds : [ 'i-fleet-1' ] } ] } ) ;
1123+
1124+ const result = await createRunner ( createRunnerConfig ( {
1125+ ...dedicatedHostRunnerConfig ,
1126+ useDedicatedHost : undefined ,
1127+ } ) ) ;
1128+
1129+ expect ( result ) . toEqual ( [ 'i-fleet-1' ] ) ;
1130+ expect ( mockEC2Client ) . toHaveReceivedCommand ( CreateFleetCommand ) ;
1131+ expect ( mockEC2Client ) . not . toHaveReceivedCommand ( RunInstancesCommand ) ;
1132+ } ) ;
1133+
1134+ it ( 'passes correct parameters to RunInstances' , async ( ) => {
1135+ await createRunner ( createRunnerConfig ( dedicatedHostRunnerConfig ) ) ;
1136+
1137+ expect ( mockEC2Client ) . toHaveReceivedCommandWith ( RunInstancesCommand , {
1138+ LaunchTemplate : {
1139+ LaunchTemplateName : LAUNCH_TEMPLATE ,
1140+ Version : '$Default' ,
1141+ } ,
1142+ InstanceType : 'm5.large' ,
1143+ MinCount : 1 ,
1144+ MaxCount : 1 ,
1145+ SubnetId : 'subnet-123' ,
1146+ TagSpecifications : [
1147+ {
1148+ ResourceType : 'instance' ,
1149+ Tags : [
1150+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1151+ { Key : 'ghr:created_by' , Value : 'scale-up-lambda' } ,
1152+ { Key : 'ghr:Type' , Value : 'Org' } ,
1153+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1154+ ] ,
1155+ } ,
1156+ {
1157+ ResourceType : 'volume' ,
1158+ Tags : [
1159+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1160+ { Key : 'ghr:created_by' , Value : 'scale-up-lambda' } ,
1161+ { Key : 'ghr:Type' , Value : 'Org' } ,
1162+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1163+ ] ,
1164+ } ,
1165+ ] ,
1166+ } ) ;
1167+ } ) ;
1168+
1169+ it ( 'creates multiple instances via RunInstances' , async ( ) => {
1170+ mockEC2Client . on ( RunInstancesCommand ) . resolves ( {
1171+ Instances : [ { InstanceId : 'i-dedicated-1' } , { InstanceId : 'i-dedicated-2' } ] ,
1172+ } ) ;
1173+
1174+ const result = await createRunner ( {
1175+ ...createRunnerConfig ( dedicatedHostRunnerConfig ) ,
1176+ numberOfRunners : 2 ,
1177+ } ) ;
1178+
1179+ expect ( result ) . toEqual ( [ 'i-dedicated-1' , 'i-dedicated-2' ] ) ;
1180+ expect ( mockEC2Client ) . toHaveReceivedCommandWith ( RunInstancesCommand , {
1181+ LaunchTemplate : {
1182+ LaunchTemplateName : LAUNCH_TEMPLATE ,
1183+ Version : '$Default' ,
1184+ } ,
1185+ InstanceType : 'm5.large' ,
1186+ MinCount : 2 ,
1187+ MaxCount : 2 ,
1188+ SubnetId : 'subnet-123' ,
1189+ TagSpecifications : [
1190+ {
1191+ ResourceType : 'instance' ,
1192+ Tags : [
1193+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1194+ { Key : 'ghr:created_by' , Value : 'pool-lambda' } ,
1195+ { Key : 'ghr:Type' , Value : 'Org' } ,
1196+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1197+ ] ,
1198+ } ,
1199+ {
1200+ ResourceType : 'volume' ,
1201+ Tags : [
1202+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1203+ { Key : 'ghr:created_by' , Value : 'pool-lambda' } ,
1204+ { Key : 'ghr:Type' , Value : 'Org' } ,
1205+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1206+ ] ,
1207+ } ,
1208+ ] ,
1209+ } ) ;
1210+ } ) ;
1211+
1212+ it ( 'throws error when spot is used with dedicated host' , async ( ) => {
1213+ await expect (
1214+ createRunner ( createRunnerConfig ( {
1215+ ...dedicatedHostRunnerConfig ,
1216+ capacityType : 'spot' ,
1217+ } ) ) ,
1218+ ) . rejects . toThrow ( 'Spot instances are not supported with RunInstances' ) ;
1219+ expect ( mockEC2Client ) . not . toHaveReceivedCommand ( RunInstancesCommand ) ;
1220+ } ) ;
1221+
1222+ it ( 'throws error when RunInstances returns no instances' , async ( ) => {
1223+ mockEC2Client . on ( RunInstancesCommand ) . resolves ( { Instances : [ ] } ) ;
1224+
1225+ await expect (
1226+ createRunner ( createRunnerConfig ( dedicatedHostRunnerConfig ) ) ,
1227+ ) . rejects . toThrow ( 'RunInstances returned no instances for dedicated host.' ) ;
1228+ } ) ;
1229+
1230+ it ( 'throws error when RunInstances fails' , async ( ) => {
1231+ mockEC2Client . on ( RunInstancesCommand ) . rejects ( new Error ( 'EC2 error' ) ) ;
1232+
1233+ await expect (
1234+ createRunner ( createRunnerConfig ( dedicatedHostRunnerConfig ) ) ,
1235+ ) . rejects . toThrow ( 'EC2 error' ) ;
1236+ } ) ;
1237+
1238+ it ( 'uses ami id override from ssm parameter' , async ( ) => {
1239+ const paramValue : GetParameterResult = {
1240+ Parameter : {
1241+ Value : 'ami-dedicated' ,
1242+ } ,
1243+ } ;
1244+ mockSSMClient . on ( GetParameterCommand ) . resolves ( paramValue ) ;
1245+
1246+ await createRunner ( createRunnerConfig ( {
1247+ ...dedicatedHostRunnerConfig ,
1248+ amiIdSsmParameterName : 'my-ami-id-param' ,
1249+ } ) ) ;
1250+
1251+ expect ( mockEC2Client ) . toHaveReceivedCommandWith ( RunInstancesCommand , {
1252+ LaunchTemplate : {
1253+ LaunchTemplateName : LAUNCH_TEMPLATE ,
1254+ Version : '$Default' ,
1255+ } ,
1256+ InstanceType : 'm5.large' ,
1257+ MinCount : 1 ,
1258+ MaxCount : 1 ,
1259+ SubnetId : 'subnet-123' ,
1260+ ImageId : 'ami-dedicated' ,
1261+ TagSpecifications : [
1262+ {
1263+ ResourceType : 'instance' ,
1264+ Tags : [
1265+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1266+ { Key : 'ghr:created_by' , Value : 'scale-up-lambda' } ,
1267+ { Key : 'ghr:Type' , Value : 'Org' } ,
1268+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1269+ ] ,
1270+ } ,
1271+ {
1272+ ResourceType : 'volume' ,
1273+ Tags : [
1274+ { Key : 'ghr:Application' , Value : 'github-action-runner' } ,
1275+ { Key : 'ghr:created_by' , Value : 'scale-up-lambda' } ,
1276+ { Key : 'ghr:Type' , Value : 'Org' } ,
1277+ { Key : 'ghr:Owner' , Value : REPO_NAME } ,
1278+ ] ,
1279+ } ,
1280+ ] ,
1281+ } ) ;
1282+ expect ( mockSSMClient ) . toHaveReceivedCommandWith ( GetParameterCommand , {
1283+ Name : 'my-ami-id-param' ,
1284+ } ) ;
1285+ } ) ;
1286+ } ) ;
0 commit comments