sdk.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package ecs
  14. import (
  15. "context"
  16. "fmt"
  17. "strings"
  18. "time"
  19. "github.com/aws/aws-sdk-go/aws/client"
  20. "github.com/docker/compose-cli/compose"
  21. "github.com/docker/compose-cli/secrets"
  22. "github.com/aws/aws-sdk-go/aws"
  23. "github.com/aws/aws-sdk-go/service/cloudformation"
  24. "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
  25. "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
  26. "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
  27. "github.com/aws/aws-sdk-go/service/ec2"
  28. "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
  29. "github.com/aws/aws-sdk-go/service/ecs"
  30. "github.com/aws/aws-sdk-go/service/ecs/ecsiface"
  31. "github.com/aws/aws-sdk-go/service/elbv2"
  32. "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
  33. "github.com/aws/aws-sdk-go/service/iam"
  34. "github.com/aws/aws-sdk-go/service/iam/iamiface"
  35. "github.com/aws/aws-sdk-go/service/secretsmanager"
  36. "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
  37. cf "github.com/awslabs/goformation/v4/cloudformation"
  38. "github.com/sirupsen/logrus"
  39. )
  40. type sdk struct {
  41. ECS ecsiface.ECSAPI
  42. EC2 ec2iface.EC2API
  43. ELB elbv2iface.ELBV2API
  44. CW cloudwatchlogsiface.CloudWatchLogsAPI
  45. IAM iamiface.IAMAPI
  46. CF cloudformationiface.CloudFormationAPI
  47. SM secretsmanageriface.SecretsManagerAPI
  48. }
  49. func newSDK(sess client.ConfigProvider) sdk {
  50. return sdk{
  51. ECS: ecs.New(sess),
  52. EC2: ec2.New(sess),
  53. ELB: elbv2.New(sess),
  54. CW: cloudwatchlogs.New(sess),
  55. IAM: iam.New(sess),
  56. CF: cloudformation.New(sess),
  57. SM: secretsmanager.New(sess),
  58. }
  59. }
  60. func (s sdk) CheckRequirements(ctx context.Context, region string) error {
  61. settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{
  62. EffectiveSettings: aws.Bool(true),
  63. Name: aws.String("serviceLongArnFormat"),
  64. })
  65. if err != nil {
  66. return err
  67. }
  68. serviceLongArnFormat := settings.Settings[0].Value
  69. if *serviceLongArnFormat != "enabled" {
  70. return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
  71. "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+
  72. "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region)
  73. }
  74. return nil
  75. }
  76. func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) {
  77. logrus.Debug("CheckRequirements if cluster was already created: ", name)
  78. clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{
  79. Clusters: []*string{aws.String(name)},
  80. })
  81. if err != nil {
  82. return false, err
  83. }
  84. return len(clusters.Clusters) > 0, nil
  85. }
  86. func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) {
  87. logrus.Debug("Create cluster ", name)
  88. response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)})
  89. if err != nil {
  90. return "", err
  91. }
  92. return *response.Cluster.Status, nil
  93. }
  94. func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) {
  95. logrus.Debug("CheckRequirements if VPC exists: ", vpcID)
  96. _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}})
  97. return err == nil, err
  98. }
  99. func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) {
  100. logrus.Debug("Retrieve default VPC")
  101. vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{
  102. Filters: []*ec2.Filter{
  103. {
  104. Name: aws.String("isDefault"),
  105. Values: []*string{aws.String("true")},
  106. },
  107. },
  108. })
  109. if err != nil {
  110. return "", err
  111. }
  112. if len(vpcs.Vpcs) == 0 {
  113. return "", fmt.Errorf("account has not default VPC")
  114. }
  115. return *vpcs.Vpcs[0].VpcId, nil
  116. }
  117. func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) {
  118. logrus.Debug("Retrieve SubNets")
  119. subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{
  120. DryRun: nil,
  121. Filters: []*ec2.Filter{
  122. {
  123. Name: aws.String("vpc-id"),
  124. Values: []*string{aws.String(vpcID)},
  125. },
  126. },
  127. })
  128. if err != nil {
  129. return nil, err
  130. }
  131. ids := []string{}
  132. for _, subnet := range subnets.Subnets {
  133. ids = append(ids, *subnet.SubnetId)
  134. }
  135. return ids, nil
  136. }
  137. func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) {
  138. role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{
  139. RoleName: aws.String(name),
  140. })
  141. if err != nil {
  142. return "", err
  143. }
  144. return *role.Role.Arn, nil
  145. }
  146. func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
  147. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  148. StackName: aws.String(name),
  149. })
  150. if err != nil {
  151. if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) {
  152. return false, nil
  153. }
  154. return false, nil
  155. }
  156. return len(stacks.Stacks) > 0, nil
  157. }
  158. func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error {
  159. logrus.Debug("Create CloudFormation stack")
  160. json, err := marshall(template)
  161. if err != nil {
  162. return err
  163. }
  164. param := []*cloudformation.Parameter{}
  165. for name, value := range parameters {
  166. param = append(param, &cloudformation.Parameter{
  167. ParameterKey: aws.String(name),
  168. ParameterValue: aws.String(value),
  169. })
  170. }
  171. _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  172. OnFailure: aws.String("DELETE"),
  173. StackName: aws.String(name),
  174. TemplateBody: aws.String(string(json)),
  175. Parameters: param,
  176. TimeoutInMinutes: nil,
  177. Capabilities: []*string{
  178. aws.String(cloudformation.CapabilityCapabilityIam),
  179. },
  180. })
  181. return err
  182. }
  183. func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) {
  184. logrus.Debug("Create CloudFormation Changeset")
  185. json, err := marshall(template)
  186. if err != nil {
  187. return "", err
  188. }
  189. param := []*cloudformation.Parameter{}
  190. for name := range parameters {
  191. param = append(param, &cloudformation.Parameter{
  192. ParameterKey: aws.String(name),
  193. UsePreviousValue: aws.Bool(true),
  194. })
  195. }
  196. update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
  197. changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
  198. ChangeSetName: aws.String(update),
  199. ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
  200. StackName: aws.String(name),
  201. TemplateBody: aws.String(string(json)),
  202. Parameters: param,
  203. Capabilities: []*string{
  204. aws.String(cloudformation.CapabilityCapabilityIam),
  205. },
  206. })
  207. if err != nil {
  208. return "", err
  209. }
  210. err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  211. ChangeSetName: changeset.Id,
  212. })
  213. return *changeset.Id, err
  214. }
  215. func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
  216. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  217. ChangeSetName: aws.String(changeset),
  218. })
  219. if err != nil {
  220. return err
  221. }
  222. if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") {
  223. return nil
  224. }
  225. _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{
  226. ChangeSetName: aws.String(changeset),
  227. })
  228. return err
  229. }
  230. const (
  231. stackCreate = iota
  232. stackUpdate
  233. stackDelete
  234. )
  235. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  236. input := &cloudformation.DescribeStacksInput{
  237. StackName: aws.String(name),
  238. }
  239. switch operation {
  240. case stackCreate:
  241. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  242. case stackDelete:
  243. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  244. default:
  245. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  246. }
  247. }
  248. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  249. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  250. StackName: aws.String(name),
  251. })
  252. if err != nil {
  253. return "", err
  254. }
  255. return *stacks.Stacks[0].StackId, nil
  256. }
  257. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  258. // Fixme implement Paginator on Events and return as a chan(events)
  259. events := []*cloudformation.StackEvent{}
  260. var nextToken *string
  261. for {
  262. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  263. StackName: aws.String(stackID),
  264. NextToken: nextToken,
  265. })
  266. if err != nil {
  267. return nil, err
  268. }
  269. events = append(events, resp.StackEvents...)
  270. if resp.NextToken == nil {
  271. return events, nil
  272. }
  273. nextToken = resp.NextToken
  274. }
  275. }
  276. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  277. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  278. NextToken: nil,
  279. StackName: aws.String(name),
  280. })
  281. if err != nil {
  282. return nil, err
  283. }
  284. parameters := map[string]string{}
  285. for _, parameter := range st.Stacks[0].Parameters {
  286. parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue)
  287. }
  288. return parameters, nil
  289. }
  290. type stackResource struct {
  291. LogicalID string
  292. Type string
  293. ARN string
  294. Status string
  295. }
  296. func (s sdk) ListStackResources(ctx context.Context, name string) ([]stackResource, error) {
  297. // FIXME handle pagination
  298. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  299. StackName: aws.String(name),
  300. })
  301. if err != nil {
  302. return nil, err
  303. }
  304. resources := []stackResource{}
  305. for _, r := range res.StackResourceSummaries {
  306. resources = append(resources, stackResource{
  307. LogicalID: aws.StringValue(r.LogicalResourceId),
  308. Type: aws.StringValue(r.ResourceType),
  309. ARN: aws.StringValue(r.PhysicalResourceId),
  310. Status: aws.StringValue(r.ResourceStatus),
  311. })
  312. }
  313. return resources, nil
  314. }
  315. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  316. logrus.Debug("Delete CloudFormation stack")
  317. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  318. StackName: aws.String(name),
  319. })
  320. return err
  321. }
  322. func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) {
  323. logrus.Debug("Create secret " + secret.Name)
  324. secretStr, err := secret.GetCredString()
  325. if err != nil {
  326. return "", err
  327. }
  328. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  329. Name: &secret.Name,
  330. SecretString: &secretStr,
  331. Description: &secret.Description,
  332. })
  333. if err != nil {
  334. return "", err
  335. }
  336. return aws.StringValue(response.ARN), nil
  337. }
  338. func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) {
  339. logrus.Debug("Inspect secret " + id)
  340. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  341. if err != nil {
  342. return secrets.Secret{}, err
  343. }
  344. labels := map[string]string{}
  345. for _, tag := range response.Tags {
  346. labels[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
  347. }
  348. secret := secrets.Secret{
  349. ID: aws.StringValue(response.ARN),
  350. Name: aws.StringValue(response.Name),
  351. Labels: labels,
  352. }
  353. if response.Description != nil {
  354. secret.Description = *response.Description
  355. }
  356. return secret, nil
  357. }
  358. func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) {
  359. logrus.Debug("List secrets ...")
  360. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  361. if err != nil {
  362. return nil, err
  363. }
  364. var ls []secrets.Secret
  365. for _, sec := range response.SecretList {
  366. labels := map[string]string{}
  367. for _, tag := range sec.Tags {
  368. labels[*tag.Key] = *tag.Value
  369. }
  370. description := ""
  371. if sec.Description != nil {
  372. description = *sec.Description
  373. }
  374. ls = append(ls, secrets.Secret{
  375. ID: *sec.ARN,
  376. Name: *sec.Name,
  377. Labels: labels,
  378. Description: description,
  379. })
  380. }
  381. return ls, nil
  382. }
  383. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  384. logrus.Debug("List secrets ...")
  385. force := !recover
  386. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  387. return err
  388. }
  389. func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error {
  390. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  391. var startTime int64
  392. for {
  393. select {
  394. case <-ctx.Done():
  395. return nil
  396. default:
  397. var hasMore = true
  398. var token *string
  399. for hasMore {
  400. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  401. LogGroupName: aws.String(logGroup),
  402. NextToken: token,
  403. StartTime: aws.Int64(startTime),
  404. })
  405. if err != nil {
  406. return err
  407. }
  408. if events.NextToken == nil {
  409. hasMore = false
  410. } else {
  411. token = events.NextToken
  412. }
  413. for _, event := range events.Events {
  414. p := strings.Split(aws.StringValue(event.LogStreamName), "/")
  415. consumer(p[1], p[2], aws.StringValue(event.Message))
  416. startTime = *event.IngestionTime
  417. }
  418. }
  419. }
  420. time.Sleep(500 * time.Millisecond)
  421. }
  422. }
  423. func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) {
  424. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  425. Cluster: aws.String(cluster),
  426. Services: aws.StringSlice(arns),
  427. Include: aws.StringSlice([]string{"TAGS"}),
  428. })
  429. if err != nil {
  430. return nil, err
  431. }
  432. status := []compose.ServiceStatus{}
  433. for _, service := range services.Services {
  434. var name string
  435. for _, t := range service.Tags {
  436. if *t.Key == compose.ServiceTag {
  437. name = aws.StringValue(t.Value)
  438. }
  439. }
  440. if name == "" {
  441. return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  442. }
  443. targetGroupArns := []string{}
  444. for _, lb := range service.LoadBalancers {
  445. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  446. }
  447. // getURLwithPortMapping makes 2 queries
  448. // one to get the target groups and another for load balancers
  449. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  450. if err != nil {
  451. return nil, err
  452. }
  453. status = append(status, compose.ServiceStatus{
  454. ID: aws.StringValue(service.ServiceName),
  455. Name: name,
  456. Replicas: int(aws.Int64Value(service.RunningCount)),
  457. Desired: int(aws.Int64Value(service.DesiredCount)),
  458. Publishers: loadBalancers,
  459. })
  460. }
  461. return status, nil
  462. }
  463. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  464. if len(targetGroupArns) == 0 {
  465. return nil, nil
  466. }
  467. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  468. TargetGroupArns: aws.StringSlice(targetGroupArns),
  469. })
  470. if err != nil {
  471. return nil, err
  472. }
  473. lbarns := []*string{}
  474. for _, tg := range groups.TargetGroups {
  475. lbarns = append(lbarns, tg.LoadBalancerArns...)
  476. }
  477. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  478. LoadBalancerArns: lbarns,
  479. })
  480. if err != nil {
  481. return nil, err
  482. }
  483. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  484. if aws.StringValue(arn) == "" {
  485. // load balancer arn is nil/""
  486. return nil
  487. }
  488. for _, lb := range lbs {
  489. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  490. return lb
  491. }
  492. }
  493. return nil
  494. }
  495. loadBalancers := []compose.PortPublisher{}
  496. for _, tg := range groups.TargetGroups {
  497. for _, lbarn := range tg.LoadBalancerArns {
  498. lb := filterLB(lbarn, lbs.LoadBalancers)
  499. if lb == nil {
  500. continue
  501. }
  502. loadBalancers = append(loadBalancers, compose.PortPublisher{
  503. URL: aws.StringValue(lb.DNSName),
  504. TargetPort: int(aws.Int64Value(tg.Port)),
  505. PublishedPort: int(aws.Int64Value(tg.Port)),
  506. Protocol: aws.StringValue(tg.Protocol),
  507. })
  508. }
  509. }
  510. return loadBalancers, nil
  511. }
  512. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  513. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  514. Cluster: aws.String(cluster),
  515. Family: aws.String(family),
  516. })
  517. if err != nil {
  518. return nil, err
  519. }
  520. arns := []string{}
  521. for _, arn := range tasks.TaskArns {
  522. arns = append(arns, *arn)
  523. }
  524. return arns, nil
  525. }
  526. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  527. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  528. NetworkInterfaceIds: aws.StringSlice(interfaces),
  529. })
  530. if err != nil {
  531. return nil, err
  532. }
  533. publicIPs := map[string]string{}
  534. for _, interf := range desc.NetworkInterfaces {
  535. if interf.Association != nil {
  536. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  537. }
  538. }
  539. return publicIPs, nil
  540. }
  541. func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) {
  542. logrus.Debug("CheckRequirements if PortPublisher exists: ", arn)
  543. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  544. LoadBalancerArns: []*string{aws.String(arn)},
  545. })
  546. if err != nil {
  547. return false, err
  548. }
  549. return len(lbs.LoadBalancers) > 0, nil
  550. }
  551. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  552. logrus.Debug("Retrieve load balancer URL: ", arn)
  553. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  554. LoadBalancerArns: []*string{aws.String(arn)},
  555. })
  556. if err != nil {
  557. return "", err
  558. }
  559. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  560. if dnsName == "" {
  561. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  562. }
  563. return dnsName, nil
  564. }