github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/modules/aws/ec2.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 6 "github.com/aws/aws-sdk-go/aws" 7 "github.com/aws/aws-sdk-go/service/ec2" 8 "github.com/gruntwork-io/terratest/modules/logger" 9 "github.com/gruntwork-io/terratest/modules/testing" 10 "github.com/stretchr/testify/require" 11 ) 12 13 // GetPrivateIpOfEc2Instance gets the private IP address of the given EC2 Instance in the given region. 14 func GetPrivateIpOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string { 15 ip, err := GetPrivateIpOfEc2InstanceE(t, instanceID, awsRegion) 16 require.NoError(t, err) 17 return ip 18 } 19 20 // GetPrivateIpOfEc2InstanceE gets the private IP address of the given EC2 Instance in the given region. 21 func GetPrivateIpOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) { 22 ips, err := GetPrivateIpsOfEc2InstancesE(t, []string{instanceID}, awsRegion) 23 if err != nil { 24 return "", err 25 } 26 27 ip, containsIP := ips[instanceID] 28 29 if !containsIP { 30 return "", IpForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: "private"} 31 } 32 33 return ip, nil 34 } 35 36 // GetPrivateIpsOfEc2Instances gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 37 func GetPrivateIpsOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string { 38 ips, err := GetPrivateIpsOfEc2InstancesE(t, instanceIDs, awsRegion) 39 require.NoError(t, err) 40 return ips 41 } 42 43 // GetPrivateIpsOfEc2InstancesE gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 44 func GetPrivateIpsOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) { 45 ec2Client := NewEc2Client(t, awsRegion) 46 // TODO: implement pagination for cases that extend beyond limit (1000 instances) 47 input := ec2.DescribeInstancesInput{InstanceIds: aws.StringSlice(instanceIDs)} 48 output, err := ec2Client.DescribeInstances(&input) 49 if err != nil { 50 return nil, err 51 } 52 53 ips := map[string]string{} 54 55 for _, reserveration := range output.Reservations { 56 for _, instance := range reserveration.Instances { 57 ips[aws.StringValue(instance.InstanceId)] = aws.StringValue(instance.PrivateIpAddress) 58 } 59 } 60 61 return ips, nil 62 } 63 64 // GetPrivateHostnameOfEc2Instance gets the private IP address of the given EC2 Instance in the given region. 65 func GetPrivateHostnameOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string { 66 ip, err := GetPrivateHostnameOfEc2InstanceE(t, instanceID, awsRegion) 67 require.NoError(t, err) 68 return ip 69 } 70 71 // GetPrivateHostnameOfEc2InstanceE gets the private IP address of the given EC2 Instance in the given region. 72 func GetPrivateHostnameOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) { 73 hostnames, err := GetPrivateHostnamesOfEc2InstancesE(t, []string{instanceID}, awsRegion) 74 if err != nil { 75 return "", err 76 } 77 78 hostname, containsHostname := hostnames[instanceID] 79 80 if !containsHostname { 81 return "", HostnameForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: "private"} 82 } 83 84 return hostname, nil 85 } 86 87 // GetPrivateHostnamesOfEc2Instances gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 88 func GetPrivateHostnamesOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string { 89 ips, err := GetPrivateHostnamesOfEc2InstancesE(t, instanceIDs, awsRegion) 90 require.NoError(t, err) 91 return ips 92 } 93 94 // GetPrivateHostnamesOfEc2InstancesE gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 95 func GetPrivateHostnamesOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) { 96 ec2Client, err := NewEc2ClientE(t, awsRegion) 97 if err != nil { 98 return nil, err 99 } 100 // TODO: implement pagination for cases that extend beyond limit (1000 instances) 101 input := ec2.DescribeInstancesInput{InstanceIds: aws.StringSlice(instanceIDs)} 102 output, err := ec2Client.DescribeInstances(&input) 103 if err != nil { 104 return nil, err 105 } 106 107 hostnames := map[string]string{} 108 109 for _, reserveration := range output.Reservations { 110 for _, instance := range reserveration.Instances { 111 hostnames[aws.StringValue(instance.InstanceId)] = aws.StringValue(instance.PrivateDnsName) 112 } 113 } 114 115 return hostnames, nil 116 } 117 118 // GetPublicIpOfEc2Instance gets the public IP address of the given EC2 Instance in the given region. 119 func GetPublicIpOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string { 120 ip, err := GetPublicIpOfEc2InstanceE(t, instanceID, awsRegion) 121 require.NoError(t, err) 122 return ip 123 } 124 125 // GetPublicIpOfEc2InstanceE gets the public IP address of the given EC2 Instance in the given region. 126 func GetPublicIpOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) { 127 ips, err := GetPublicIpsOfEc2InstancesE(t, []string{instanceID}, awsRegion) 128 if err != nil { 129 return "", err 130 } 131 132 ip, containsIP := ips[instanceID] 133 134 if !containsIP { 135 return "", IpForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: "public"} 136 } 137 138 return ip, nil 139 } 140 141 // GetPublicIpsOfEc2Instances gets the public IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 142 func GetPublicIpsOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string { 143 ips, err := GetPublicIpsOfEc2InstancesE(t, instanceIDs, awsRegion) 144 require.NoError(t, err) 145 return ips 146 } 147 148 // GetPublicIpsOfEc2InstancesE gets the public IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address. 149 func GetPublicIpsOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) { 150 ec2Client := NewEc2Client(t, awsRegion) 151 // TODO: implement pagination for cases that extend beyond limit (1000 instances) 152 input := ec2.DescribeInstancesInput{InstanceIds: aws.StringSlice(instanceIDs)} 153 output, err := ec2Client.DescribeInstances(&input) 154 if err != nil { 155 return nil, err 156 } 157 158 ips := map[string]string{} 159 160 for _, reserveration := range output.Reservations { 161 for _, instance := range reserveration.Instances { 162 ips[aws.StringValue(instance.InstanceId)] = aws.StringValue(instance.PublicIpAddress) 163 } 164 } 165 166 return ips, nil 167 } 168 169 // GetEc2InstanceIdsByTag returns all the IDs of EC2 instances in the given region with the given tag. 170 func GetEc2InstanceIdsByTag(t testing.TestingT, region string, tagName string, tagValue string) []string { 171 out, err := GetEc2InstanceIdsByTagE(t, region, tagName, tagValue) 172 require.NoError(t, err) 173 return out 174 } 175 176 // GetEc2InstanceIdsByTagE returns all the IDs of EC2 instances in the given region with the given tag. 177 func GetEc2InstanceIdsByTagE(t testing.TestingT, region string, tagName string, tagValue string) ([]string, error) { 178 ec2Filters := map[string][]string{ 179 fmt.Sprintf("tag:%s", tagName): {tagValue}, 180 } 181 return GetEc2InstanceIdsByFiltersE(t, region, ec2Filters) 182 } 183 184 // GetEc2InstanceIdsByFilters returns all the IDs of EC2 instances in the given region which match to EC2 filter list 185 // as per https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeInstancesInput. 186 func GetEc2InstanceIdsByFilters(t testing.TestingT, region string, ec2Filters map[string][]string) []string { 187 out, err := GetEc2InstanceIdsByFiltersE(t, region, ec2Filters) 188 require.NoError(t, err) 189 return out 190 } 191 192 // GetEc2InstanceIdsByFilters returns all the IDs of EC2 instances in the given region which match to EC2 filter list 193 // as per https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeInstancesInput. 194 func GetEc2InstanceIdsByFiltersE(t testing.TestingT, region string, ec2Filters map[string][]string) ([]string, error) { 195 client, err := NewEc2ClientE(t, region) 196 if err != nil { 197 return nil, err 198 } 199 200 ec2FilterList := []*ec2.Filter{} 201 202 for name, values := range ec2Filters { 203 ec2FilterList = append(ec2FilterList, &ec2.Filter{Name: aws.String(name), Values: aws.StringSlice(values)}) 204 } 205 206 // TODO: implement pagination for cases that extend beyond limit (1000 instances) 207 output, err := client.DescribeInstances(&ec2.DescribeInstancesInput{Filters: ec2FilterList}) 208 if err != nil { 209 return nil, err 210 } 211 212 instanceIDs := []string{} 213 214 for _, reservation := range output.Reservations { 215 for _, instance := range reservation.Instances { 216 instanceIDs = append(instanceIDs, *instance.InstanceId) 217 } 218 } 219 220 return instanceIDs, err 221 } 222 223 // GetTagsForEc2Instance returns all the tags for the given EC2 Instance. 224 func GetTagsForEc2Instance(t testing.TestingT, region string, instanceID string) map[string]string { 225 tags, err := GetTagsForEc2InstanceE(t, region, instanceID) 226 require.NoError(t, err) 227 return tags 228 } 229 230 // GetTagsForEc2InstanceE returns all the tags for the given EC2 Instance. 231 func GetTagsForEc2InstanceE(t testing.TestingT, region string, instanceID string) (map[string]string, error) { 232 client, err := NewEc2ClientE(t, region) 233 if err != nil { 234 return nil, err 235 } 236 237 input := ec2.DescribeTagsInput{ 238 Filters: []*ec2.Filter{ 239 { 240 Name: aws.String("resource-type"), 241 Values: aws.StringSlice([]string{"instance"}), 242 }, 243 { 244 Name: aws.String("resource-id"), 245 Values: aws.StringSlice([]string{instanceID}), 246 }, 247 }, 248 } 249 250 out, err := client.DescribeTags(&input) 251 if err != nil { 252 return nil, err 253 } 254 255 tags := map[string]string{} 256 257 for _, tag := range out.Tags { 258 tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) 259 } 260 261 return tags, nil 262 } 263 264 // DeleteAmi deletes the given AMI in the given region. 265 func DeleteAmi(t testing.TestingT, region string, imageID string) { 266 require.NoError(t, DeleteAmiE(t, region, imageID)) 267 } 268 269 // DeleteAmiE deletes the given AMI in the given region. 270 func DeleteAmiE(t testing.TestingT, region string, imageID string) error { 271 logger.Logf(t, "Deregistering AMI %s", imageID) 272 273 client, err := NewEc2ClientE(t, region) 274 if err != nil { 275 return err 276 } 277 278 _, err = client.DeregisterImage(&ec2.DeregisterImageInput{ImageId: aws.String(imageID)}) 279 return err 280 } 281 282 // AddTagsToResource adds the tags to the given taggable AWS resource such as EC2, AMI or VPC. 283 func AddTagsToResource(t testing.TestingT, region string, resource string, tags map[string]string) { 284 require.NoError(t, AddTagsToResourceE(t, region, resource, tags)) 285 } 286 287 // AddTagsToResourceE adds the tags to the given taggable AWS resource such as EC2, AMI or VPC. 288 func AddTagsToResourceE(t testing.TestingT, region string, resource string, tags map[string]string) error { 289 client, err := NewEc2ClientE(t, region) 290 if err != nil { 291 return err 292 } 293 294 var awsTags []*ec2.Tag 295 for key, value := range tags { 296 awsTags = append(awsTags, &ec2.Tag{ 297 Key: aws.String(key), 298 Value: aws.String(value), 299 }) 300 } 301 302 _, err = client.CreateTags(&ec2.CreateTagsInput{ 303 Resources: []*string{aws.String(resource)}, 304 Tags: awsTags, 305 }) 306 307 return err 308 } 309 310 // TerminateInstance terminates the EC2 instance with the given ID in the given region. 311 func TerminateInstance(t testing.TestingT, region string, instanceID string) { 312 require.NoError(t, TerminateInstanceE(t, region, instanceID)) 313 } 314 315 // TerminateInstanceE terminates the EC2 instance with the given ID in the given region. 316 func TerminateInstanceE(t testing.TestingT, region string, instanceID string) error { 317 logger.Logf(t, "Terminating Instance %s", instanceID) 318 319 client, err := NewEc2ClientE(t, region) 320 if err != nil { 321 return err 322 } 323 324 _, err = client.TerminateInstances(&ec2.TerminateInstancesInput{ 325 InstanceIds: []*string{ 326 aws.String(instanceID), 327 }, 328 }) 329 330 return err 331 } 332 333 // GetAmiPubliclyAccessible returns whether the AMI is publicly accessible or not 334 func GetAmiPubliclyAccessible(t testing.TestingT, awsRegion string, amiID string) bool { 335 output, err := GetAmiPubliclyAccessibleE(t, awsRegion, amiID) 336 require.NoError(t, err) 337 return output 338 } 339 340 // GetAmiPubliclyAccessibleE returns whether the AMI is publicly accessible or not 341 func GetAmiPubliclyAccessibleE(t testing.TestingT, awsRegion string, amiID string) (bool, error) { 342 launchPermissions, err := GetLaunchPermissionsForAmiE(t, awsRegion, amiID) 343 if err != nil { 344 return false, err 345 } 346 for _, launchPermission := range launchPermissions { 347 if aws.StringValue(launchPermission.Group) == "all" { 348 return true, nil 349 } 350 } 351 return false, nil 352 } 353 354 // GetAccountsWithLaunchPermissionsForAmi returns list of accounts that the AMI is shared with 355 func GetAccountsWithLaunchPermissionsForAmi(t testing.TestingT, awsRegion string, amiID string) []string { 356 output, err := GetAccountsWithLaunchPermissionsForAmiE(t, awsRegion, amiID) 357 require.NoError(t, err) 358 return output 359 } 360 361 // GetAccountsWithLaunchPermissionsForAmiE returns list of accounts that the AMI is shared with 362 func GetAccountsWithLaunchPermissionsForAmiE(t testing.TestingT, awsRegion string, amiID string) ([]string, error) { 363 accountIDs := []string{} 364 launchPermissions, err := GetLaunchPermissionsForAmiE(t, awsRegion, amiID) 365 if err != nil { 366 return accountIDs, err 367 } 368 for _, launchPermission := range launchPermissions { 369 if aws.StringValue(launchPermission.UserId) != "" { 370 accountIDs = append(accountIDs, aws.StringValue(launchPermission.UserId)) 371 } 372 } 373 return accountIDs, nil 374 } 375 376 // GetLaunchPermissionsForAmiE returns launchPermissions as configured in AWS 377 func GetLaunchPermissionsForAmiE(t testing.TestingT, awsRegion string, amiID string) ([]*ec2.LaunchPermission, error) { 378 client := NewEc2Client(t, awsRegion) 379 input := &ec2.DescribeImageAttributeInput{ 380 Attribute: aws.String("launchPermission"), 381 ImageId: aws.String(amiID), 382 } 383 384 output, err := client.DescribeImageAttribute(input) 385 if err != nil { 386 return []*ec2.LaunchPermission{}, err 387 } 388 return output.LaunchPermissions, nil 389 } 390 391 // GetRecommendedInstanceType takes in a list of EC2 instance types (e.g., "t2.micro", "t3.micro") and returns the 392 // first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no 393 // instance available in all AZs, this function exits with an error. This is useful because certain instance types, 394 // such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older 395 // AZs, and if you have code that needs to run on a "small" instance across all AZs in many different regions, you can 396 // use this function to automatically figure out which instance type you should use. 397 // This function will fail the test if there is an error. 398 func GetRecommendedInstanceType(t testing.TestingT, region string, instanceTypeOptions []string) string { 399 out, err := GetRecommendedInstanceTypeE(t, region, instanceTypeOptions) 400 require.NoError(t, err) 401 return out 402 } 403 404 // GetRecommendedInstanceTypeE takes in a list of EC2 instance types (e.g., "t2.micro", "t3.micro") and returns the 405 // first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no 406 // instance available in all AZs, this function exits with an error. This is useful because certain instance types, 407 // such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older 408 // AZs. If you have code that needs to run on a "small" instance across all AZs in many different regions, you can 409 // use this function to automatically figure out which instance type you should use. 410 func GetRecommendedInstanceTypeE(t testing.TestingT, region string, instanceTypeOptions []string) (string, error) { 411 client, err := NewEc2ClientE(t, region) 412 if err != nil { 413 return "", err 414 } 415 return GetRecommendedInstanceTypeWithClientE(t, client, instanceTypeOptions) 416 } 417 418 // GetRecommendedInstanceTypeWithClientE takes in a list of EC2 instance types (e.g., "t2.micro", "t3.micro") and returns the 419 // first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no 420 // instance available in all AZs, this function exits with an error. This is useful because certain instance types, 421 // such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older 422 // AZs. If you have code that needs to run on a "small" instance across all AZs in many different regions, you can 423 // use this function to automatically figure out which instance type you should use. 424 // This function expects an authenticated EC2 client from the AWS SDK Go library. 425 func GetRecommendedInstanceTypeWithClientE(t testing.TestingT, ec2Client *ec2.EC2, instanceTypeOptions []string) (string, error) { 426 availabilityZones, err := getAllAvailabilityZonesE(ec2Client) 427 if err != nil { 428 return "", err 429 } 430 431 instanceTypeOfferings, err := getInstanceTypeOfferingsE(ec2Client, instanceTypeOptions) 432 if err != nil { 433 return "", err 434 } 435 436 return pickRecommendedInstanceTypeE(availabilityZones, instanceTypeOfferings, instanceTypeOptions) 437 } 438 439 // pickRecommendedInstanceTypeE returns the first instance type from instanceTypeOptions that is available in all the 440 // AZs in availabilityZones based on the availability data in instanceTypeOfferings. If none of the instance types are 441 // available in all AZs, this function returns an error. 442 func pickRecommendedInstanceTypeE(availabilityZones []string, instanceTypeOfferings []*ec2.InstanceTypeOffering, instanceTypeOptions []string) (string, error) { 443 // O(n^3) for the win! 444 for _, instanceType := range instanceTypeOptions { 445 if instanceTypeExistsInAllAzs(instanceType, availabilityZones, instanceTypeOfferings) { 446 return instanceType, nil 447 } 448 } 449 450 return "", NoInstanceTypeError{InstanceTypeOptions: instanceTypeOptions, Azs: availabilityZones} 451 } 452 453 // instanceTypeExistsInAllAzs returns true if the given inistance type exists in all the given availabilityZones based 454 // on the availability data in instanceTypeOfferings 455 func instanceTypeExistsInAllAzs(instanceType string, availabilityZones []string, instanceTypeOfferings []*ec2.InstanceTypeOffering) bool { 456 if len(availabilityZones) == 0 || len(instanceTypeOfferings) == 0 { 457 return false 458 } 459 460 for _, az := range availabilityZones { 461 if !hasOffering(instanceTypeOfferings, az, instanceType) { 462 return false 463 } 464 } 465 466 return true 467 } 468 469 // hasOffering returns true if the given availability zone and instance type are one of the offerings in 470 // instanceTypeOfferings 471 func hasOffering(instanceTypeOfferings []*ec2.InstanceTypeOffering, availabilityZone string, instanceType string) bool { 472 for _, offering := range instanceTypeOfferings { 473 if aws.StringValue(offering.InstanceType) == instanceType && aws.StringValue(offering.Location) == availabilityZone { 474 return true 475 } 476 } 477 478 return false 479 } 480 481 // getInstanceTypeOfferingsE returns the instance types from the given list that are available in the region configured 482 // in the given EC2 client 483 func getInstanceTypeOfferingsE(client *ec2.EC2, instanceTypeOptions []string) ([]*ec2.InstanceTypeOffering, error) { 484 input := ec2.DescribeInstanceTypeOfferingsInput{ 485 LocationType: aws.String(ec2.LocationTypeAvailabilityZone), 486 Filters: []*ec2.Filter{ 487 { 488 Name: aws.String("instance-type"), 489 Values: aws.StringSlice(instanceTypeOptions), 490 }, 491 }, 492 } 493 494 out, err := client.DescribeInstanceTypeOfferings(&input) 495 if err != nil { 496 return nil, err 497 } 498 499 return out.InstanceTypeOfferings, nil 500 } 501 502 // getAllAvailabilityZonesE returns all the available AZs in the region configured in the given EC2 client 503 func getAllAvailabilityZonesE(client *ec2.EC2) ([]string, error) { 504 input := ec2.DescribeAvailabilityZonesInput{ 505 Filters: []*ec2.Filter{ 506 { 507 Name: aws.String("state"), 508 Values: aws.StringSlice([]string{"available"}), 509 }, 510 }, 511 } 512 513 out, err := client.DescribeAvailabilityZones(&input) 514 if err != nil { 515 return nil, err 516 } 517 518 var azs []string 519 520 for _, az := range out.AvailabilityZones { 521 azs = append(azs, aws.StringValue(az.ZoneName)) 522 } 523 524 return azs, nil 525 } 526 527 // NewEc2Client creates an EC2 client. 528 func NewEc2Client(t testing.TestingT, region string) *ec2.EC2 { 529 client, err := NewEc2ClientE(t, region) 530 require.NoError(t, err) 531 return client 532 } 533 534 // NewEc2ClientE creates an EC2 client. 535 func NewEc2ClientE(t testing.TestingT, region string) (*ec2.EC2, error) { 536 sess, err := NewAuthenticatedSession(region) 537 if err != nil { 538 return nil, err 539 } 540 541 return ec2.New(sess), nil 542 }