sdk.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  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. "encoding/json"
  17. "fmt"
  18. "strings"
  19. "time"
  20. "github.com/aws/aws-sdk-go/service/ssm"
  21. "github.com/aws/aws-sdk-go/service/ssm/ssmiface"
  22. "github.com/docker/compose-cli/api/compose"
  23. "github.com/docker/compose-cli/api/secrets"
  24. "github.com/aws/aws-sdk-go/aws"
  25. "github.com/aws/aws-sdk-go/aws/request"
  26. "github.com/aws/aws-sdk-go/aws/session"
  27. "github.com/aws/aws-sdk-go/service/autoscaling"
  28. "github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
  29. "github.com/aws/aws-sdk-go/service/cloudformation"
  30. "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
  31. "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
  32. "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
  33. "github.com/aws/aws-sdk-go/service/ec2"
  34. "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
  35. "github.com/aws/aws-sdk-go/service/ecs"
  36. "github.com/aws/aws-sdk-go/service/ecs/ecsiface"
  37. "github.com/aws/aws-sdk-go/service/efs"
  38. "github.com/aws/aws-sdk-go/service/efs/efsiface"
  39. "github.com/aws/aws-sdk-go/service/elbv2"
  40. "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
  41. "github.com/aws/aws-sdk-go/service/iam"
  42. "github.com/aws/aws-sdk-go/service/iam/iamiface"
  43. "github.com/aws/aws-sdk-go/service/secretsmanager"
  44. "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
  45. "github.com/hashicorp/go-multierror"
  46. "github.com/sirupsen/logrus"
  47. )
  48. type sdk struct {
  49. ECS ecsiface.ECSAPI
  50. EC2 ec2iface.EC2API
  51. EFS efsiface.EFSAPI
  52. ELB elbv2iface.ELBV2API
  53. CW cloudwatchlogsiface.CloudWatchLogsAPI
  54. IAM iamiface.IAMAPI
  55. CF cloudformationiface.CloudFormationAPI
  56. SM secretsmanageriface.SecretsManagerAPI
  57. SSM ssmiface.SSMAPI
  58. AG autoscalingiface.AutoScalingAPI
  59. }
  60. func newSDK(sess *session.Session) sdk {
  61. sess.Handlers.Build.PushBack(func(r *request.Request) {
  62. request.AddToUserAgent(r, "Docker CLI")
  63. })
  64. return sdk{
  65. ECS: ecs.New(sess),
  66. EC2: ec2.New(sess),
  67. EFS: efs.New(sess),
  68. ELB: elbv2.New(sess),
  69. CW: cloudwatchlogs.New(sess),
  70. IAM: iam.New(sess),
  71. CF: cloudformation.New(sess),
  72. SM: secretsmanager.New(sess),
  73. SSM: ssm.New(sess),
  74. AG: autoscaling.New(sess),
  75. }
  76. }
  77. func (s sdk) CheckRequirements(ctx context.Context, region string) error {
  78. settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{
  79. EffectiveSettings: aws.Bool(true),
  80. Name: aws.String("serviceLongArnFormat"),
  81. })
  82. if err != nil {
  83. return err
  84. }
  85. serviceLongArnFormat := settings.Settings[0].Value
  86. if *serviceLongArnFormat != "enabled" {
  87. return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
  88. "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+
  89. "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region)
  90. }
  91. return nil
  92. }
  93. func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) {
  94. logrus.Debug("CheckRequirements if cluster was already created: ", name)
  95. clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{
  96. Clusters: []*string{aws.String(name)},
  97. })
  98. if err != nil {
  99. return false, err
  100. }
  101. return len(clusters.Clusters) > 0, nil
  102. }
  103. func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) {
  104. logrus.Debug("Create cluster ", name)
  105. response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)})
  106. if err != nil {
  107. return "", err
  108. }
  109. return *response.Cluster.Status, nil
  110. }
  111. func (s sdk) CheckVPC(ctx context.Context, vpcID string) error {
  112. logrus.Debug("CheckRequirements on VPC : ", vpcID)
  113. output, err := s.EC2.DescribeVpcAttributeWithContext(ctx, &ec2.DescribeVpcAttributeInput{
  114. VpcId: aws.String(vpcID),
  115. Attribute: aws.String("enableDnsSupport"),
  116. })
  117. if err != nil {
  118. return err
  119. }
  120. if !*output.EnableDnsSupport.Value {
  121. return fmt.Errorf("VPC %q doesn't have DNS resolution enabled", vpcID)
  122. }
  123. return err
  124. }
  125. func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) {
  126. logrus.Debug("Retrieve default VPC")
  127. vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{
  128. Filters: []*ec2.Filter{
  129. {
  130. Name: aws.String("isDefault"),
  131. Values: []*string{aws.String("true")},
  132. },
  133. },
  134. })
  135. if err != nil {
  136. return "", err
  137. }
  138. if len(vpcs.Vpcs) == 0 {
  139. return "", fmt.Errorf("account has not default VPC")
  140. }
  141. return *vpcs.Vpcs[0].VpcId, nil
  142. }
  143. func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) {
  144. logrus.Debug("Retrieve SubNets")
  145. subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{
  146. DryRun: nil,
  147. Filters: []*ec2.Filter{
  148. {
  149. Name: aws.String("vpc-id"),
  150. Values: []*string{aws.String(vpcID)},
  151. },
  152. },
  153. })
  154. if err != nil {
  155. return nil, err
  156. }
  157. ids := []string{}
  158. for _, subnet := range subnets.Subnets {
  159. ids = append(ids, *subnet.SubnetId)
  160. }
  161. return ids, nil
  162. }
  163. func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) {
  164. role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{
  165. RoleName: aws.String(name),
  166. })
  167. if err != nil {
  168. return "", err
  169. }
  170. return *role.Role.Arn, nil
  171. }
  172. func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
  173. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  174. StackName: aws.String(name),
  175. })
  176. if err != nil {
  177. if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with ID %s does not exist", name)) {
  178. return false, nil
  179. }
  180. return false, nil
  181. }
  182. return len(stacks.Stacks) > 0, nil
  183. }
  184. func (s sdk) CreateStack(ctx context.Context, name string, template []byte) error {
  185. logrus.Debug("Create CloudFormation stack")
  186. _, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  187. OnFailure: aws.String("DELETE"),
  188. StackName: aws.String(name),
  189. TemplateBody: aws.String(string(template)),
  190. TimeoutInMinutes: nil,
  191. Capabilities: []*string{
  192. aws.String(cloudformation.CapabilityCapabilityIam),
  193. },
  194. Tags: []*cloudformation.Tag{
  195. {
  196. Key: aws.String(compose.ProjectTag),
  197. Value: aws.String(name),
  198. },
  199. },
  200. })
  201. return err
  202. }
  203. func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte) (string, error) {
  204. logrus.Debug("Create CloudFormation Changeset")
  205. update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
  206. changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
  207. ChangeSetName: aws.String(update),
  208. ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
  209. StackName: aws.String(name),
  210. TemplateBody: aws.String(string(template)),
  211. Capabilities: []*string{
  212. aws.String(cloudformation.CapabilityCapabilityIam),
  213. },
  214. })
  215. if err != nil {
  216. return "", err
  217. }
  218. err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  219. ChangeSetName: changeset.Id,
  220. })
  221. return *changeset.Id, err
  222. }
  223. func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
  224. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  225. ChangeSetName: aws.String(changeset),
  226. })
  227. if err != nil {
  228. return err
  229. }
  230. if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") {
  231. return nil
  232. }
  233. _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{
  234. ChangeSetName: aws.String(changeset),
  235. })
  236. return err
  237. }
  238. const (
  239. stackCreate = iota
  240. stackUpdate
  241. stackDelete
  242. )
  243. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  244. input := &cloudformation.DescribeStacksInput{
  245. StackName: aws.String(name),
  246. }
  247. switch operation {
  248. case stackCreate:
  249. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  250. case stackDelete:
  251. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  252. default:
  253. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  254. }
  255. }
  256. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  257. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  258. StackName: aws.String(name),
  259. })
  260. if err != nil {
  261. return "", err
  262. }
  263. return *stacks.Stacks[0].StackId, nil
  264. }
  265. func (s sdk) ListStacks(ctx context.Context, name string) ([]compose.Stack, error) {
  266. params := cloudformation.DescribeStacksInput{}
  267. if name != "" {
  268. params.StackName = &name
  269. }
  270. cfStacks, err := s.CF.DescribeStacksWithContext(ctx, &params)
  271. if err != nil {
  272. return nil, err
  273. }
  274. stacks := []compose.Stack{}
  275. for _, stack := range cfStacks.Stacks {
  276. skip := true
  277. for _, t := range stack.Tags {
  278. if *t.Key == compose.ProjectTag {
  279. skip = false
  280. break
  281. }
  282. }
  283. if skip {
  284. continue
  285. }
  286. status := compose.RUNNING
  287. reason := ""
  288. switch aws.StringValue(stack.StackStatus) {
  289. case "CREATE_IN_PROGRESS":
  290. status = compose.STARTING
  291. case "DELETE_IN_PROGRESS":
  292. status = compose.REMOVING
  293. case "UPDATE_IN_PROGRESS":
  294. status = compose.UPDATING
  295. }
  296. if status == compose.STARTING {
  297. if err := s.CheckStackState(ctx, aws.StringValue(stack.StackName)); err != nil {
  298. status = compose.FAILED
  299. reason = err.Error()
  300. }
  301. }
  302. stacks = append(stacks, compose.Stack{
  303. ID: aws.StringValue(stack.StackId),
  304. Name: aws.StringValue(stack.StackName),
  305. Status: status,
  306. Reason: reason,
  307. })
  308. }
  309. return stacks, nil
  310. }
  311. func (s sdk) CheckStackState(ctx context.Context, name string) error {
  312. resources, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  313. StackName: aws.String(name),
  314. })
  315. if err != nil {
  316. return err
  317. }
  318. services := []*string{}
  319. serviceNames := []string{}
  320. var cluster *string
  321. for _, r := range resources.StackResourceSummaries {
  322. if aws.StringValue(r.ResourceType) == "AWS::ECS::Cluster" {
  323. cluster = r.PhysicalResourceId
  324. continue
  325. }
  326. if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
  327. if r.PhysicalResourceId == nil {
  328. continue
  329. }
  330. services = append(services, r.PhysicalResourceId)
  331. serviceNames = append(serviceNames, *r.LogicalResourceId)
  332. }
  333. }
  334. for i, service := range services {
  335. err := s.CheckTaskState(ctx, aws.StringValue(cluster), aws.StringValue(service))
  336. if err != nil {
  337. return fmt.Errorf("%s error: %s", serviceNames[i], err.Error())
  338. }
  339. }
  340. return nil
  341. }
  342. func (s sdk) CheckTaskState(ctx context.Context, cluster string, serviceName string) error {
  343. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  344. Cluster: aws.String(cluster),
  345. ServiceName: aws.String(serviceName),
  346. })
  347. if err != nil {
  348. return err
  349. }
  350. if len(tasks.TaskArns) > 0 {
  351. return nil
  352. }
  353. tasks, err = s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  354. Cluster: aws.String(cluster),
  355. ServiceName: aws.String(serviceName),
  356. DesiredStatus: aws.String("STOPPED"),
  357. })
  358. if err != nil {
  359. return err
  360. }
  361. if len(tasks.TaskArns) > 0 {
  362. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  363. Cluster: aws.String(cluster),
  364. Tasks: tasks.TaskArns,
  365. })
  366. if err != nil {
  367. return err
  368. }
  369. if len(taskDescriptions.Tasks) > 0 {
  370. recentTask := taskDescriptions.Tasks[0]
  371. switch aws.StringValue(recentTask.StopCode) {
  372. case "TaskFailedToStart":
  373. return fmt.Errorf(aws.StringValue(recentTask.StoppedReason))
  374. }
  375. }
  376. }
  377. return nil
  378. }
  379. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  380. // Fixme implement Paginator on Events and return as a chan(events)
  381. events := []*cloudformation.StackEvent{}
  382. var nextToken *string
  383. for {
  384. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  385. StackName: aws.String(stackID),
  386. NextToken: nextToken,
  387. })
  388. if err != nil {
  389. return nil, err
  390. }
  391. events = append(events, resp.StackEvents...)
  392. if resp.NextToken == nil {
  393. return events, nil
  394. }
  395. nextToken = resp.NextToken
  396. }
  397. }
  398. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  399. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  400. NextToken: nil,
  401. StackName: aws.String(name),
  402. })
  403. if err != nil {
  404. return nil, err
  405. }
  406. parameters := map[string]string{}
  407. for _, parameter := range st.Stacks[0].Parameters {
  408. parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue)
  409. }
  410. return parameters, nil
  411. }
  412. type stackResource struct {
  413. LogicalID string
  414. Type string
  415. ARN string
  416. Status string
  417. }
  418. type stackResourceFn func(r stackResource) error
  419. type stackResources []stackResource
  420. func (resources stackResources) apply(awsType string, fn stackResourceFn) error {
  421. var errs *multierror.Error
  422. for _, r := range resources {
  423. if r.Type == awsType {
  424. err := fn(r)
  425. if err != nil {
  426. errs = multierror.Append(err)
  427. }
  428. }
  429. }
  430. return errs.ErrorOrNil()
  431. }
  432. func (s sdk) ListStackResources(ctx context.Context, name string) (stackResources, error) {
  433. // FIXME handle pagination
  434. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  435. StackName: aws.String(name),
  436. })
  437. if err != nil {
  438. return nil, err
  439. }
  440. resources := stackResources{}
  441. for _, r := range res.StackResourceSummaries {
  442. resources = append(resources, stackResource{
  443. LogicalID: aws.StringValue(r.LogicalResourceId),
  444. Type: aws.StringValue(r.ResourceType),
  445. ARN: aws.StringValue(r.PhysicalResourceId),
  446. Status: aws.StringValue(r.ResourceStatus),
  447. })
  448. }
  449. return resources, nil
  450. }
  451. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  452. logrus.Debug("Delete CloudFormation stack")
  453. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  454. StackName: aws.String(name),
  455. })
  456. return err
  457. }
  458. func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) {
  459. logrus.Debug("Create secret " + secret.Name)
  460. secretStr, err := secret.GetCredString()
  461. if err != nil {
  462. return "", err
  463. }
  464. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  465. Name: &secret.Name,
  466. SecretString: &secretStr,
  467. Description: &secret.Description,
  468. })
  469. if err != nil {
  470. return "", err
  471. }
  472. return aws.StringValue(response.ARN), nil
  473. }
  474. func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) {
  475. logrus.Debug("Inspect secret " + id)
  476. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  477. if err != nil {
  478. return secrets.Secret{}, err
  479. }
  480. labels := map[string]string{}
  481. for _, tag := range response.Tags {
  482. labels[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
  483. }
  484. secret := secrets.Secret{
  485. ID: aws.StringValue(response.ARN),
  486. Name: aws.StringValue(response.Name),
  487. Labels: labels,
  488. }
  489. if response.Description != nil {
  490. secret.Description = *response.Description
  491. }
  492. return secret, nil
  493. }
  494. func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) {
  495. logrus.Debug("List secrets ...")
  496. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  497. if err != nil {
  498. return nil, err
  499. }
  500. var ls []secrets.Secret
  501. for _, sec := range response.SecretList {
  502. labels := map[string]string{}
  503. for _, tag := range sec.Tags {
  504. labels[*tag.Key] = *tag.Value
  505. }
  506. description := ""
  507. if sec.Description != nil {
  508. description = *sec.Description
  509. }
  510. ls = append(ls, secrets.Secret{
  511. ID: *sec.ARN,
  512. Name: *sec.Name,
  513. Labels: labels,
  514. Description: description,
  515. })
  516. }
  517. return ls, nil
  518. }
  519. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  520. logrus.Debug("List secrets ...")
  521. force := !recover
  522. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  523. return err
  524. }
  525. func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error {
  526. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  527. var startTime int64
  528. for {
  529. select {
  530. case <-ctx.Done():
  531. return nil
  532. default:
  533. var hasMore = true
  534. var token *string
  535. for hasMore {
  536. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  537. LogGroupName: aws.String(logGroup),
  538. NextToken: token,
  539. StartTime: aws.Int64(startTime),
  540. })
  541. if err != nil {
  542. return err
  543. }
  544. if events.NextToken == nil {
  545. hasMore = false
  546. } else {
  547. token = events.NextToken
  548. }
  549. for _, event := range events.Events {
  550. p := strings.Split(aws.StringValue(event.LogStreamName), "/")
  551. consumer(p[1], p[2], aws.StringValue(event.Message))
  552. startTime = *event.IngestionTime
  553. }
  554. }
  555. }
  556. time.Sleep(500 * time.Millisecond)
  557. }
  558. }
  559. func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) {
  560. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  561. Cluster: aws.String(cluster),
  562. Services: aws.StringSlice(arns),
  563. Include: aws.StringSlice([]string{"TAGS"}),
  564. })
  565. if err != nil {
  566. return nil, err
  567. }
  568. status := []compose.ServiceStatus{}
  569. for _, service := range services.Services {
  570. var name string
  571. for _, t := range service.Tags {
  572. if *t.Key == compose.ServiceTag {
  573. name = aws.StringValue(t.Value)
  574. }
  575. }
  576. if name == "" {
  577. return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  578. }
  579. targetGroupArns := []string{}
  580. for _, lb := range service.LoadBalancers {
  581. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  582. }
  583. // getURLwithPortMapping makes 2 queries
  584. // one to get the target groups and another for load balancers
  585. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  586. if err != nil {
  587. return nil, err
  588. }
  589. status = append(status, compose.ServiceStatus{
  590. ID: aws.StringValue(service.ServiceName),
  591. Name: name,
  592. Replicas: int(aws.Int64Value(service.RunningCount)),
  593. Desired: int(aws.Int64Value(service.DesiredCount)),
  594. Publishers: loadBalancers,
  595. })
  596. }
  597. return status, nil
  598. }
  599. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  600. if len(targetGroupArns) == 0 {
  601. return nil, nil
  602. }
  603. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  604. TargetGroupArns: aws.StringSlice(targetGroupArns),
  605. })
  606. if err != nil {
  607. return nil, err
  608. }
  609. lbarns := []*string{}
  610. for _, tg := range groups.TargetGroups {
  611. lbarns = append(lbarns, tg.LoadBalancerArns...)
  612. }
  613. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  614. LoadBalancerArns: lbarns,
  615. })
  616. if err != nil {
  617. return nil, err
  618. }
  619. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  620. if aws.StringValue(arn) == "" {
  621. // load balancer arn is nil/""
  622. return nil
  623. }
  624. for _, lb := range lbs {
  625. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  626. return lb
  627. }
  628. }
  629. return nil
  630. }
  631. loadBalancers := []compose.PortPublisher{}
  632. for _, tg := range groups.TargetGroups {
  633. for _, lbarn := range tg.LoadBalancerArns {
  634. lb := filterLB(lbarn, lbs.LoadBalancers)
  635. if lb == nil {
  636. continue
  637. }
  638. loadBalancers = append(loadBalancers, compose.PortPublisher{
  639. URL: aws.StringValue(lb.DNSName),
  640. TargetPort: int(aws.Int64Value(tg.Port)),
  641. PublishedPort: int(aws.Int64Value(tg.Port)),
  642. Protocol: aws.StringValue(tg.Protocol),
  643. })
  644. }
  645. }
  646. return loadBalancers, nil
  647. }
  648. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  649. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  650. Cluster: aws.String(cluster),
  651. Family: aws.String(family),
  652. })
  653. if err != nil {
  654. return nil, err
  655. }
  656. arns := []string{}
  657. for _, arn := range tasks.TaskArns {
  658. arns = append(arns, *arn)
  659. }
  660. return arns, nil
  661. }
  662. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  663. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  664. NetworkInterfaceIds: aws.StringSlice(interfaces),
  665. })
  666. if err != nil {
  667. return nil, err
  668. }
  669. publicIPs := map[string]string{}
  670. for _, interf := range desc.NetworkInterfaces {
  671. if interf.Association != nil {
  672. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  673. }
  674. }
  675. return publicIPs, nil
  676. }
  677. func (s sdk) LoadBalancerType(ctx context.Context, arn string) (string, error) {
  678. logrus.Debug("Check if LoadBalancer exists: ", arn)
  679. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  680. LoadBalancerArns: []*string{aws.String(arn)},
  681. })
  682. if err != nil {
  683. return "", err
  684. }
  685. if len(lbs.LoadBalancers) == 0 {
  686. return "", fmt.Errorf("load balancer does not exist: %s", arn)
  687. }
  688. return aws.StringValue(lbs.LoadBalancers[0].Type), nil
  689. }
  690. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  691. logrus.Debug("Retrieve load balancer URL: ", arn)
  692. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  693. LoadBalancerArns: []*string{aws.String(arn)},
  694. })
  695. if err != nil {
  696. return "", err
  697. }
  698. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  699. if dnsName == "" {
  700. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  701. }
  702. return dnsName, nil
  703. }
  704. func (s sdk) WithVolumeSecurityGroups(ctx context.Context, id string, fn func(securityGroups []string) error) error {
  705. mounts, err := s.EFS.DescribeMountTargetsWithContext(ctx, &efs.DescribeMountTargetsInput{
  706. FileSystemId: aws.String(id),
  707. })
  708. if err != nil {
  709. return err
  710. }
  711. for _, mount := range mounts.MountTargets {
  712. groups, err := s.EFS.DescribeMountTargetSecurityGroupsWithContext(ctx, &efs.DescribeMountTargetSecurityGroupsInput{
  713. MountTargetId: mount.MountTargetId,
  714. })
  715. if err != nil {
  716. return err
  717. }
  718. err = fn(aws.StringValueSlice(groups.SecurityGroups))
  719. if err != nil {
  720. return err
  721. }
  722. }
  723. return nil
  724. }
  725. func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
  726. parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  727. Name: aws.String(name),
  728. })
  729. if err != nil {
  730. return "", err
  731. }
  732. value := *parameter.Parameter.Value
  733. var ami struct {
  734. SchemaVersion int `json:"schema_version"`
  735. ImageName string `json:"image_name"`
  736. ImageID string `json:"image_id"`
  737. OS string `json:"os"`
  738. ECSRuntimeVersion string `json:"ecs_runtime_verion"`
  739. ECSAgentVersion string `json:"ecs_agent_version"`
  740. }
  741. err = json.Unmarshal([]byte(value), &ami)
  742. if err != nil {
  743. return "", err
  744. }
  745. return ami.ImageID, nil
  746. }
  747. func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
  748. desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
  749. GroupIds: aws.StringSlice([]string{sg}),
  750. })
  751. if err != nil {
  752. return false, err
  753. }
  754. return len(desc.SecurityGroups) > 0, nil
  755. }
  756. func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
  757. _, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
  758. CapacityProvider: aws.String(arn),
  759. })
  760. return err
  761. }
  762. func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
  763. _, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
  764. AutoScalingGroupName: aws.String(arn),
  765. ForceDelete: aws.Bool(true),
  766. })
  767. return err
  768. }