sdk.go 17 KB

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