sdk.go 17 KB

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