sdk.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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. Name: aws.String("default-for-az"),
  120. Values: []*string{aws.String("true")},
  121. },
  122. },
  123. })
  124. if err != nil {
  125. return nil, err
  126. }
  127. ids := []string{}
  128. for _, subnet := range subnets.Subnets {
  129. ids = append(ids, *subnet.SubnetId)
  130. }
  131. return ids, nil
  132. }
  133. func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) {
  134. role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{
  135. RoleName: aws.String(name),
  136. })
  137. if err != nil {
  138. return "", err
  139. }
  140. return *role.Role.Arn, nil
  141. }
  142. func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
  143. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  144. StackName: aws.String(name),
  145. })
  146. if err != nil {
  147. if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) {
  148. return false, nil
  149. }
  150. return false, nil
  151. }
  152. return len(stacks.Stacks) > 0, nil
  153. }
  154. func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error {
  155. logrus.Debug("Create CloudFormation stack")
  156. json, err := template.JSON()
  157. if err != nil {
  158. return err
  159. }
  160. param := []*cloudformation.Parameter{}
  161. for name, value := range parameters {
  162. param = append(param, &cloudformation.Parameter{
  163. ParameterKey: aws.String(name),
  164. ParameterValue: aws.String(value),
  165. })
  166. }
  167. _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  168. OnFailure: aws.String("DELETE"),
  169. StackName: aws.String(name),
  170. TemplateBody: aws.String(string(json)),
  171. Parameters: param,
  172. TimeoutInMinutes: aws.Int64(15),
  173. Capabilities: []*string{
  174. aws.String(cloudformation.CapabilityCapabilityIam),
  175. },
  176. })
  177. return err
  178. }
  179. func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) {
  180. logrus.Debug("Create CloudFormation Changeset")
  181. json, err := template.JSON()
  182. if err != nil {
  183. return "", err
  184. }
  185. param := []*cloudformation.Parameter{}
  186. for name := range parameters {
  187. param = append(param, &cloudformation.Parameter{
  188. ParameterKey: aws.String(name),
  189. UsePreviousValue: aws.Bool(true),
  190. })
  191. }
  192. update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
  193. changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
  194. ChangeSetName: aws.String(update),
  195. ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
  196. StackName: aws.String(name),
  197. TemplateBody: aws.String(string(json)),
  198. Parameters: param,
  199. Capabilities: []*string{
  200. aws.String(cloudformation.CapabilityCapabilityIam),
  201. },
  202. })
  203. if err != nil {
  204. return "", err
  205. }
  206. err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  207. ChangeSetName: changeset.Id,
  208. })
  209. return *changeset.Id, err
  210. }
  211. func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
  212. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  213. ChangeSetName: aws.String(changeset),
  214. })
  215. if err != nil {
  216. return err
  217. }
  218. if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") {
  219. return nil
  220. }
  221. _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{
  222. ChangeSetName: aws.String(changeset),
  223. })
  224. return err
  225. }
  226. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  227. input := &cloudformation.DescribeStacksInput{
  228. StackName: aws.String(name),
  229. }
  230. switch operation {
  231. case compose.StackCreate:
  232. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  233. case compose.StackDelete:
  234. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  235. default:
  236. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  237. }
  238. }
  239. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  240. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  241. StackName: aws.String(name),
  242. })
  243. if err != nil {
  244. return "", err
  245. }
  246. return *stacks.Stacks[0].StackId, nil
  247. }
  248. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  249. // Fixme implement Paginator on Events and return as a chan(events)
  250. events := []*cloudformation.StackEvent{}
  251. var nextToken *string
  252. for {
  253. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  254. StackName: aws.String(stackID),
  255. NextToken: nextToken,
  256. })
  257. if err != nil {
  258. return nil, err
  259. }
  260. events = append(events, resp.StackEvents...)
  261. if resp.NextToken == nil {
  262. return events, nil
  263. }
  264. nextToken = resp.NextToken
  265. }
  266. }
  267. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  268. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  269. NextToken: nil,
  270. StackName: aws.String(name),
  271. })
  272. if err != nil {
  273. return nil, err
  274. }
  275. parameters := map[string]string{}
  276. for _, parameter := range st.Stacks[0].Parameters {
  277. parameters[*parameter.ParameterKey] = *parameter.ParameterValue
  278. }
  279. return parameters, nil
  280. }
  281. func (s sdk) ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) {
  282. // FIXME handle pagination
  283. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  284. StackName: aws.String(name),
  285. })
  286. if err != nil {
  287. return nil, err
  288. }
  289. resources := []compose.StackResource{}
  290. for _, r := range res.StackResourceSummaries {
  291. resources = append(resources, compose.StackResource{
  292. LogicalID: *r.LogicalResourceId,
  293. Type: *r.ResourceType,
  294. ARN: *r.PhysicalResourceId,
  295. Status: *r.ResourceStatus,
  296. })
  297. }
  298. return resources, nil
  299. }
  300. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  301. logrus.Debug("Delete CloudFormation stack")
  302. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  303. StackName: aws.String(name),
  304. })
  305. return err
  306. }
  307. func (s sdk) CreateSecret(ctx context.Context, secret compose.Secret) (string, error) {
  308. logrus.Debug("Create secret " + secret.Name)
  309. secretStr, err := secret.GetCredString()
  310. if err != nil {
  311. return "", err
  312. }
  313. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  314. Name: &secret.Name,
  315. SecretString: &secretStr,
  316. Description: &secret.Description,
  317. })
  318. if err != nil {
  319. return "", err
  320. }
  321. return *response.ARN, nil
  322. }
  323. func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, error) {
  324. logrus.Debug("Inspect secret " + id)
  325. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  326. if err != nil {
  327. return compose.Secret{}, err
  328. }
  329. labels := map[string]string{}
  330. for _, tag := range response.Tags {
  331. labels[*tag.Key] = *tag.Value
  332. }
  333. secret := compose.Secret{
  334. ID: *response.ARN,
  335. Name: *response.Name,
  336. Labels: labels,
  337. }
  338. if response.Description != nil {
  339. secret.Description = *response.Description
  340. }
  341. return secret, nil
  342. }
  343. func (s sdk) ListSecrets(ctx context.Context) ([]compose.Secret, error) {
  344. logrus.Debug("List secrets ...")
  345. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  346. if err != nil {
  347. return []compose.Secret{}, err
  348. }
  349. var secrets []compose.Secret
  350. for _, sec := range response.SecretList {
  351. labels := map[string]string{}
  352. for _, tag := range sec.Tags {
  353. labels[*tag.Key] = *tag.Value
  354. }
  355. description := ""
  356. if sec.Description != nil {
  357. description = *sec.Description
  358. }
  359. secrets = append(secrets, compose.Secret{
  360. ID: *sec.ARN,
  361. Name: *sec.Name,
  362. Labels: labels,
  363. Description: description,
  364. })
  365. }
  366. return secrets, nil
  367. }
  368. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  369. logrus.Debug("List secrets ...")
  370. force := !recover
  371. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  372. return err
  373. }
  374. func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsumer) error {
  375. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  376. var startTime int64
  377. for {
  378. var hasMore = true
  379. var token *string
  380. for hasMore {
  381. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  382. LogGroupName: aws.String(logGroup),
  383. NextToken: token,
  384. StartTime: aws.Int64(startTime),
  385. })
  386. if err != nil {
  387. return err
  388. }
  389. if events.NextToken == nil {
  390. hasMore = false
  391. } else {
  392. token = events.NextToken
  393. }
  394. for _, event := range events.Events {
  395. p := strings.Split(*event.LogStreamName, "/")
  396. consumer.Log(p[1], p[2], *event.Message)
  397. startTime = *event.IngestionTime
  398. }
  399. }
  400. time.Sleep(500 * time.Millisecond)
  401. }
  402. }
  403. func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) {
  404. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  405. Cluster: aws.String(cluster),
  406. Services: aws.StringSlice(arns),
  407. Include: aws.StringSlice([]string{"TAGS"}),
  408. })
  409. if err != nil {
  410. return nil, err
  411. }
  412. status := []compose.ServiceStatus{}
  413. for _, service := range services.Services {
  414. var name string
  415. for _, t := range service.Tags {
  416. if *t.Key == compose.ServiceTag {
  417. name = *t.Value
  418. }
  419. }
  420. if name == "" {
  421. return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  422. }
  423. targetGroupArns := []string{}
  424. for _, lb := range service.LoadBalancers {
  425. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  426. }
  427. // getURLwithPortMapping makes 2 queries
  428. // one to get the target groups and another for load balancers
  429. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  430. if err != nil {
  431. return nil, err
  432. }
  433. status = append(status, compose.ServiceStatus{
  434. ID: *service.ServiceName,
  435. Name: name,
  436. Replicas: int(*service.RunningCount),
  437. Desired: int(*service.DesiredCount),
  438. LoadBalancers: loadBalancers,
  439. })
  440. }
  441. return status, nil
  442. }
  443. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.LoadBalancer, error) {
  444. if len(targetGroupArns) == 0 {
  445. return nil, nil
  446. }
  447. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  448. TargetGroupArns: aws.StringSlice(targetGroupArns),
  449. })
  450. if err != nil {
  451. return nil, err
  452. }
  453. lbarns := []*string{}
  454. for _, tg := range groups.TargetGroups {
  455. lbarns = append(lbarns, tg.LoadBalancerArns...)
  456. }
  457. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  458. LoadBalancerArns: lbarns,
  459. })
  460. if err != nil {
  461. return nil, err
  462. }
  463. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  464. for _, lb := range lbs {
  465. if *lb.LoadBalancerArn == *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: *lb.DNSName,
  480. TargetPort: int(*tg.Port),
  481. PublishedPort: int(*tg.Port),
  482. Protocol: *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[*interf.NetworkInterfaceId] = *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. return *lbs.LoadBalancers[0].DNSName, nil
  536. }