sdk.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  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. "bytes"
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "strings"
  20. "time"
  21. "github.com/docker/compose-cli/api/compose"
  22. "github.com/docker/compose-cli/api/secrets"
  23. "github.com/docker/compose-cli/errdefs"
  24. "github.com/docker/compose-cli/internal"
  25. "github.com/aws/aws-sdk-go/aws"
  26. "github.com/aws/aws-sdk-go/aws/arn"
  27. "github.com/aws/aws-sdk-go/aws/awserr"
  28. "github.com/aws/aws-sdk-go/aws/request"
  29. "github.com/aws/aws-sdk-go/aws/session"
  30. "github.com/aws/aws-sdk-go/service/autoscaling"
  31. "github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
  32. "github.com/aws/aws-sdk-go/service/cloudformation"
  33. "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
  34. "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
  35. "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
  36. "github.com/aws/aws-sdk-go/service/ec2"
  37. "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
  38. "github.com/aws/aws-sdk-go/service/ecs"
  39. "github.com/aws/aws-sdk-go/service/ecs/ecsiface"
  40. "github.com/aws/aws-sdk-go/service/efs"
  41. "github.com/aws/aws-sdk-go/service/efs/efsiface"
  42. "github.com/aws/aws-sdk-go/service/elbv2"
  43. "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
  44. "github.com/aws/aws-sdk-go/service/iam"
  45. "github.com/aws/aws-sdk-go/service/iam/iamiface"
  46. "github.com/aws/aws-sdk-go/service/s3"
  47. "github.com/aws/aws-sdk-go/service/s3/s3iface"
  48. "github.com/aws/aws-sdk-go/service/s3/s3manager"
  49. "github.com/aws/aws-sdk-go/service/secretsmanager"
  50. "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
  51. "github.com/aws/aws-sdk-go/service/ssm"
  52. "github.com/aws/aws-sdk-go/service/ssm/ssmiface"
  53. "github.com/hashicorp/go-multierror"
  54. "github.com/hashicorp/go-uuid"
  55. "github.com/pkg/errors"
  56. "github.com/sirupsen/logrus"
  57. )
  58. type sdk struct {
  59. ECS ecsiface.ECSAPI
  60. EC2 ec2iface.EC2API
  61. EFS efsiface.EFSAPI
  62. ELB elbv2iface.ELBV2API
  63. CW cloudwatchlogsiface.CloudWatchLogsAPI
  64. IAM iamiface.IAMAPI
  65. CF cloudformationiface.CloudFormationAPI
  66. SM secretsmanageriface.SecretsManagerAPI
  67. SSM ssmiface.SSMAPI
  68. AG autoscalingiface.AutoScalingAPI
  69. S3 s3iface.S3API
  70. uploader *s3manager.Uploader
  71. }
  72. // sdk implement API
  73. var _ API = sdk{}
  74. func newSDK(sess *session.Session) sdk {
  75. sess.Handlers.Build.PushBack(func(r *request.Request) {
  76. request.AddToUserAgent(r, internal.ECSUserAgentName+"/"+internal.Version)
  77. })
  78. return sdk{
  79. ECS: ecs.New(sess),
  80. EC2: ec2.New(sess),
  81. EFS: efs.New(sess),
  82. ELB: elbv2.New(sess),
  83. CW: cloudwatchlogs.New(sess),
  84. IAM: iam.New(sess),
  85. CF: cloudformation.New(sess),
  86. SM: secretsmanager.New(sess),
  87. SSM: ssm.New(sess),
  88. AG: autoscaling.New(sess),
  89. S3: s3.New(sess),
  90. uploader: s3manager.NewUploader(sess),
  91. }
  92. }
  93. func (s sdk) CheckRequirements(ctx context.Context, region string) error {
  94. settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{
  95. EffectiveSettings: aws.Bool(true),
  96. Name: aws.String("serviceLongArnFormat"),
  97. })
  98. if err != nil {
  99. return err
  100. }
  101. serviceLongArnFormat := settings.Settings[0].Value
  102. if *serviceLongArnFormat != "enabled" {
  103. return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
  104. "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+
  105. "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region)
  106. }
  107. return nil
  108. }
  109. func (s sdk) ResolveCluster(ctx context.Context, nameOrArn string) (awsResource, error) {
  110. logrus.Debug("CheckRequirements if cluster was already created: ", nameOrArn)
  111. clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{
  112. Clusters: []*string{aws.String(nameOrArn)},
  113. })
  114. if err != nil {
  115. return nil, err
  116. }
  117. if len(clusters.Clusters) == 0 {
  118. return nil, errors.Wrapf(errdefs.ErrNotFound, "cluster %q does not exist", nameOrArn)
  119. }
  120. it := clusters.Clusters[0]
  121. return existingAWSResource{
  122. arn: aws.StringValue(it.ClusterArn),
  123. id: aws.StringValue(it.ClusterName),
  124. }, nil
  125. }
  126. func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) {
  127. logrus.Debug("Create cluster ", name)
  128. response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)})
  129. if err != nil {
  130. return "", err
  131. }
  132. return *response.Cluster.Status, nil
  133. }
  134. func (s sdk) CheckVPC(ctx context.Context, vpcID string) error {
  135. logrus.Debug("CheckRequirements on VPC : ", vpcID)
  136. output, err := s.EC2.DescribeVpcAttributeWithContext(ctx, &ec2.DescribeVpcAttributeInput{
  137. VpcId: aws.String(vpcID),
  138. Attribute: aws.String("enableDnsSupport"),
  139. })
  140. if err != nil {
  141. return err
  142. }
  143. if !*output.EnableDnsSupport.Value {
  144. return fmt.Errorf("VPC %q doesn't have DNS resolution enabled", vpcID)
  145. }
  146. return nil
  147. }
  148. func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) {
  149. logrus.Debug("Retrieve default VPC")
  150. vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{
  151. Filters: []*ec2.Filter{
  152. {
  153. Name: aws.String("isDefault"),
  154. Values: []*string{aws.String("true")},
  155. },
  156. },
  157. })
  158. if err != nil {
  159. return "", err
  160. }
  161. if len(vpcs.Vpcs) == 0 {
  162. return "", fmt.Errorf("account has not default VPC")
  163. }
  164. return *vpcs.Vpcs[0].VpcId, nil
  165. }
  166. func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]awsResource, error) {
  167. logrus.Debug("Retrieve SubNets")
  168. ids := []awsResource{}
  169. var token *string
  170. for {
  171. subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{
  172. Filters: []*ec2.Filter{
  173. {
  174. Name: aws.String("vpc-id"),
  175. Values: []*string{aws.String(vpcID)},
  176. },
  177. },
  178. NextToken: token,
  179. })
  180. if err != nil {
  181. return nil, err
  182. }
  183. for _, subnet := range subnets.Subnets {
  184. ids = append(ids, existingAWSResource{
  185. arn: aws.StringValue(subnet.SubnetArn),
  186. id: aws.StringValue(subnet.SubnetId),
  187. })
  188. }
  189. if subnets.NextToken == token {
  190. break
  191. }
  192. token = subnets.NextToken
  193. }
  194. return ids, nil
  195. }
  196. func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) {
  197. role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{
  198. RoleName: aws.String(name),
  199. })
  200. if err != nil {
  201. return "", err
  202. }
  203. return *role.Role.Arn, nil
  204. }
  205. func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
  206. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  207. StackName: aws.String(name),
  208. })
  209. if err != nil {
  210. if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with ID %s does not exist", name)) {
  211. return false, nil
  212. }
  213. return false, nil
  214. }
  215. return len(stacks.Stacks) > 0, nil
  216. }
  217. type uploadedTemplateFunc func(body *string, url *string) (string, error)
  218. const cloudformationBytesLimit = 51200
  219. func (s sdk) withTemplate(ctx context.Context, name string, template []byte, region string, fn uploadedTemplateFunc) (string, error) {
  220. if len(template) < cloudformationBytesLimit {
  221. return fn(aws.String(string(template)), nil)
  222. }
  223. logrus.Debug("Create s3 bucket to store cloudformation template")
  224. var configuration *s3.CreateBucketConfiguration
  225. if region != "us-east-1" {
  226. configuration = &s3.CreateBucketConfiguration{
  227. LocationConstraint: aws.String(region),
  228. }
  229. }
  230. // CloudFormation will only allow URL from a same-region bucket
  231. // to avoid conflicts we suffix bucket name by region, so we can create comparable buckets in other regions.
  232. bucket := "com.docker.compose." + region
  233. _, err := s.S3.CreateBucket(&s3.CreateBucketInput{
  234. Bucket: aws.String(bucket),
  235. CreateBucketConfiguration: configuration,
  236. })
  237. if err != nil {
  238. ae, ok := err.(awserr.Error)
  239. if !ok {
  240. return "", err
  241. }
  242. if ae.Code() != s3.ErrCodeBucketAlreadyOwnedByYou {
  243. return "", err
  244. }
  245. }
  246. key, err := uuid.GenerateUUID()
  247. if err != nil {
  248. return "", err
  249. }
  250. upload, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{
  251. Key: aws.String(key),
  252. Body: bytes.NewReader(template),
  253. Bucket: aws.String(bucket),
  254. ContentType: aws.String("application/json"),
  255. Tagging: aws.String(name),
  256. })
  257. if err != nil {
  258. return "", err
  259. }
  260. defer s.S3.DeleteObjects(&s3.DeleteObjectsInput{ //nolint: errcheck
  261. Bucket: aws.String(bucket),
  262. Delete: &s3.Delete{
  263. Objects: []*s3.ObjectIdentifier{
  264. {
  265. Key: aws.String(key),
  266. VersionId: upload.VersionID,
  267. },
  268. },
  269. },
  270. })
  271. return fn(nil, aws.String(upload.Location))
  272. }
  273. func (s sdk) CreateStack(ctx context.Context, name string, region string, template []byte) error {
  274. logrus.Debug("Create CloudFormation stack")
  275. stackID, err := s.withTemplate(ctx, name, template, region, func(body *string, url *string) (string, error) {
  276. stack, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  277. OnFailure: aws.String("DELETE"),
  278. StackName: aws.String(name),
  279. TemplateBody: body,
  280. TemplateURL: url,
  281. TimeoutInMinutes: nil,
  282. Capabilities: []*string{
  283. aws.String(cloudformation.CapabilityCapabilityIam),
  284. },
  285. Tags: []*cloudformation.Tag{
  286. {
  287. Key: aws.String(compose.ProjectTag),
  288. Value: aws.String(name),
  289. },
  290. },
  291. })
  292. if err != nil {
  293. return "", err
  294. }
  295. return aws.StringValue(stack.StackId), nil
  296. })
  297. logrus.Debugf("Stack %s created", stackID)
  298. return err
  299. }
  300. func (s sdk) CreateChangeSet(ctx context.Context, name string, region string, template []byte) (string, error) {
  301. logrus.Debug("Create CloudFormation Changeset")
  302. update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
  303. changeset, err := s.withTemplate(ctx, name, template, region, func(body *string, url *string) (string, error) {
  304. changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
  305. ChangeSetName: aws.String(update),
  306. ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
  307. StackName: aws.String(name),
  308. TemplateBody: body,
  309. TemplateURL: url,
  310. Capabilities: []*string{
  311. aws.String(cloudformation.CapabilityCapabilityIam),
  312. },
  313. })
  314. if err != nil {
  315. return "", err
  316. }
  317. return aws.StringValue(changeset.Id), err
  318. })
  319. if err != nil {
  320. return "", err
  321. }
  322. // we have to WaitUntilChangeSetCreateComplete even this in fail with error `ResourceNotReady`
  323. // so that we can invoke DescribeChangeSet to check status, and then we can know about the actual creation failure cause.
  324. s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ // nolint:errcheck
  325. ChangeSetName: aws.String(changeset),
  326. })
  327. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  328. ChangeSetName: aws.String(update),
  329. StackName: aws.String(name),
  330. })
  331. if aws.StringValue(desc.Status) == "FAILED" {
  332. return changeset, fmt.Errorf(aws.StringValue(desc.StatusReason))
  333. }
  334. return changeset, err
  335. }
  336. func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
  337. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  338. ChangeSetName: aws.String(changeset),
  339. })
  340. if err != nil {
  341. return err
  342. }
  343. if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") {
  344. return nil
  345. }
  346. _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{
  347. ChangeSetName: aws.String(changeset),
  348. })
  349. return err
  350. }
  351. const (
  352. stackCreate = iota
  353. stackUpdate
  354. stackDelete
  355. )
  356. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  357. input := &cloudformation.DescribeStacksInput{
  358. StackName: aws.String(name),
  359. }
  360. switch operation {
  361. case stackCreate:
  362. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  363. case stackDelete:
  364. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  365. default:
  366. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  367. }
  368. }
  369. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  370. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  371. StackName: aws.String(name),
  372. })
  373. if err != nil {
  374. return "", err
  375. }
  376. return *stacks.Stacks[0].StackId, nil
  377. }
  378. func (s sdk) ListStacks(ctx context.Context, name string) ([]compose.Stack, error) {
  379. params := cloudformation.DescribeStacksInput{}
  380. if name != "" {
  381. params.StackName = &name
  382. }
  383. cfStacks, err := s.CF.DescribeStacksWithContext(ctx, &params)
  384. if err != nil {
  385. return nil, err
  386. }
  387. stacks := []compose.Stack{}
  388. for _, stack := range cfStacks.Stacks {
  389. for _, t := range stack.Tags {
  390. if *t.Key == compose.ProjectTag {
  391. status := compose.RUNNING
  392. switch aws.StringValue(stack.StackStatus) {
  393. case "CREATE_IN_PROGRESS":
  394. status = compose.STARTING
  395. case "DELETE_IN_PROGRESS":
  396. status = compose.REMOVING
  397. case "UPDATE_IN_PROGRESS":
  398. status = compose.UPDATING
  399. default:
  400. }
  401. stacks = append(stacks, compose.Stack{
  402. ID: aws.StringValue(stack.StackId),
  403. Name: aws.StringValue(stack.StackName),
  404. Status: status,
  405. })
  406. break
  407. }
  408. }
  409. }
  410. return stacks, nil
  411. }
  412. func (s sdk) GetStackClusterID(ctx context.Context, stack string) (string, error) {
  413. // Note: could use DescribeStackResource but we only can detect `does not exist` case by matching string error message
  414. resources, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  415. StackName: aws.String(stack),
  416. })
  417. if err != nil {
  418. return "", err
  419. }
  420. for _, r := range resources.StackResourceSummaries {
  421. if aws.StringValue(r.ResourceType) == "AWS::ECS::Cluster" {
  422. return aws.StringValue(r.PhysicalResourceId), nil
  423. }
  424. }
  425. // stack is using user-provided cluster
  426. res, err := s.CF.GetTemplateSummaryWithContext(ctx, &cloudformation.GetTemplateSummaryInput{
  427. StackName: aws.String(stack),
  428. })
  429. if err != nil {
  430. return "", err
  431. }
  432. c := aws.StringValue(res.Metadata)
  433. var m templateMetadata
  434. err = json.Unmarshal([]byte(c), &m)
  435. if err != nil {
  436. return "", err
  437. }
  438. if m.Cluster == "" {
  439. return "", errors.Wrap(errdefs.ErrNotFound, "CloudFormation is missing cluster metadata")
  440. }
  441. return m.Cluster, nil
  442. }
  443. type templateMetadata struct {
  444. Cluster string `json:",omitempty"`
  445. }
  446. func (s sdk) GetServiceTaskDefinition(ctx context.Context, cluster string, serviceArns []string) (map[string]string, error) {
  447. defs := map[string]string{}
  448. svc := []*string{}
  449. for _, s := range serviceArns {
  450. svc = append(svc, aws.String(s))
  451. }
  452. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  453. Cluster: aws.String(cluster),
  454. Services: svc,
  455. })
  456. if err != nil {
  457. return nil, err
  458. }
  459. for _, s := range services.Services {
  460. defs[aws.StringValue(s.ServiceArn)] = aws.StringValue(s.TaskDefinition)
  461. }
  462. return defs, nil
  463. }
  464. func (s sdk) ListStackServices(ctx context.Context, stack string) ([]string, error) {
  465. arns := []string{}
  466. var nextToken *string
  467. for {
  468. response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  469. StackName: aws.String(stack),
  470. NextToken: nextToken,
  471. })
  472. if err != nil {
  473. return nil, err
  474. }
  475. for _, r := range response.StackResourceSummaries {
  476. if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
  477. if r.PhysicalResourceId != nil {
  478. arns = append(arns, aws.StringValue(r.PhysicalResourceId))
  479. }
  480. }
  481. }
  482. nextToken = response.NextToken
  483. if nextToken == nil {
  484. break
  485. }
  486. }
  487. return arns, nil
  488. }
  489. func (s sdk) GetServiceTasks(ctx context.Context, cluster string, service string, stopped bool) ([]*ecs.Task, error) {
  490. state := "RUNNING"
  491. if stopped {
  492. state = "STOPPED"
  493. }
  494. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  495. Cluster: aws.String(cluster),
  496. ServiceName: aws.String(service),
  497. DesiredStatus: aws.String(state),
  498. })
  499. if err != nil {
  500. return nil, err
  501. }
  502. if len(tasks.TaskArns) > 0 {
  503. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  504. Cluster: aws.String(cluster),
  505. Tasks: tasks.TaskArns,
  506. })
  507. if err != nil {
  508. return nil, err
  509. }
  510. return taskDescriptions.Tasks, nil
  511. }
  512. return nil, nil
  513. }
  514. func (s sdk) GetTaskStoppedReason(ctx context.Context, cluster string, taskArn string) (string, error) {
  515. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  516. Cluster: aws.String(cluster),
  517. Tasks: []*string{aws.String(taskArn)},
  518. })
  519. if err != nil {
  520. return "", err
  521. }
  522. if len(taskDescriptions.Tasks) == 0 {
  523. return "", nil
  524. }
  525. task := taskDescriptions.Tasks[0]
  526. return fmt.Sprintf(
  527. "%s: %s",
  528. aws.StringValue(task.StopCode),
  529. aws.StringValue(task.StoppedReason)), nil
  530. }
  531. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  532. // Fixme implement Paginator on Events and return as a chan(events)
  533. events := []*cloudformation.StackEvent{}
  534. var nextToken *string
  535. for {
  536. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  537. StackName: aws.String(stackID),
  538. NextToken: nextToken,
  539. })
  540. if err != nil {
  541. return nil, err
  542. }
  543. events = append(events, resp.StackEvents...)
  544. if resp.NextToken == nil {
  545. return events, nil
  546. }
  547. nextToken = resp.NextToken
  548. }
  549. }
  550. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  551. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  552. NextToken: nil,
  553. StackName: aws.String(name),
  554. })
  555. if err != nil {
  556. return nil, err
  557. }
  558. parameters := map[string]string{}
  559. for _, parameter := range st.Stacks[0].Parameters {
  560. parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue)
  561. }
  562. return parameters, nil
  563. }
  564. type stackResource struct {
  565. LogicalID string
  566. Type string
  567. ARN string
  568. Status string
  569. }
  570. type stackResourceFn func(r stackResource) error
  571. type stackResources []stackResource
  572. func (resources stackResources) apply(awsType string, fn stackResourceFn) error {
  573. var errs *multierror.Error
  574. for _, r := range resources {
  575. if r.Type == awsType {
  576. err := fn(r)
  577. if err != nil {
  578. errs = multierror.Append(err)
  579. }
  580. }
  581. }
  582. return errs.ErrorOrNil()
  583. }
  584. func (s sdk) ListStackResources(ctx context.Context, name string) (stackResources, error) {
  585. // FIXME handle pagination
  586. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  587. StackName: aws.String(name),
  588. })
  589. if err != nil {
  590. return nil, err
  591. }
  592. resources := stackResources{}
  593. for _, r := range res.StackResourceSummaries {
  594. resources = append(resources, stackResource{
  595. LogicalID: aws.StringValue(r.LogicalResourceId),
  596. Type: aws.StringValue(r.ResourceType),
  597. ARN: aws.StringValue(r.PhysicalResourceId),
  598. Status: aws.StringValue(r.ResourceStatus),
  599. })
  600. }
  601. return resources, nil
  602. }
  603. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  604. logrus.Debug("Delete CloudFormation stack")
  605. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  606. StackName: aws.String(name),
  607. })
  608. return err
  609. }
  610. func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) {
  611. logrus.Debug("Create secret " + secret.Name)
  612. var tags []*secretsmanager.Tag
  613. for k, v := range secret.Labels {
  614. tags = []*secretsmanager.Tag{
  615. {
  616. Key: aws.String(k),
  617. Value: aws.String(v),
  618. },
  619. }
  620. }
  621. // store the secret content as string
  622. content := string(secret.GetContent())
  623. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  624. Name: &secret.Name,
  625. SecretString: &content,
  626. Tags: tags,
  627. })
  628. if err != nil {
  629. return "", err
  630. }
  631. return aws.StringValue(response.ARN), nil
  632. }
  633. func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) {
  634. logrus.Debug("Inspect secret " + id)
  635. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  636. if err != nil {
  637. return secrets.Secret{}, err
  638. }
  639. tags := map[string]string{}
  640. for _, tag := range response.Tags {
  641. tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
  642. }
  643. secret := secrets.Secret{
  644. ID: aws.StringValue(response.ARN),
  645. Name: aws.StringValue(response.Name),
  646. Labels: tags,
  647. }
  648. return secret, nil
  649. }
  650. func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) {
  651. logrus.Debug("List secrets ...")
  652. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  653. if err != nil {
  654. return nil, err
  655. }
  656. var ls []secrets.Secret
  657. for _, sec := range response.SecretList {
  658. tags := map[string]string{}
  659. for _, tag := range sec.Tags {
  660. tags[*tag.Key] = *tag.Value
  661. }
  662. ls = append(ls, secrets.Secret{
  663. ID: *sec.ARN,
  664. Name: *sec.Name,
  665. Labels: tags,
  666. })
  667. }
  668. return ls, nil
  669. }
  670. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  671. logrus.Debug("List secrets ...")
  672. force := !recover
  673. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  674. return err
  675. }
  676. func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error {
  677. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  678. var startTime int64
  679. for {
  680. select {
  681. case <-ctx.Done():
  682. return nil
  683. default:
  684. var hasMore = true
  685. var token *string
  686. for hasMore {
  687. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  688. LogGroupName: aws.String(logGroup),
  689. NextToken: token,
  690. StartTime: aws.Int64(startTime),
  691. })
  692. if err != nil {
  693. return err
  694. }
  695. if events.NextToken == nil {
  696. hasMore = false
  697. } else {
  698. token = events.NextToken
  699. }
  700. for _, event := range events.Events {
  701. p := strings.Split(aws.StringValue(event.LogStreamName), "/")
  702. consumer(p[1], p[2], aws.StringValue(event.Message))
  703. startTime = *event.IngestionTime
  704. }
  705. }
  706. }
  707. time.Sleep(500 * time.Millisecond)
  708. }
  709. }
  710. func (s sdk) DescribeService(ctx context.Context, cluster string, arn string) (compose.ServiceStatus, error) {
  711. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  712. Cluster: aws.String(cluster),
  713. Services: []*string{aws.String(arn)},
  714. Include: aws.StringSlice([]string{"TAGS"}),
  715. })
  716. if err != nil {
  717. return compose.ServiceStatus{}, err
  718. }
  719. for _, f := range services.Failures {
  720. return compose.ServiceStatus{}, errors.Wrapf(errdefs.ErrNotFound, "can't get service status %s: %s", aws.StringValue(f.Detail), aws.StringValue(f.Reason))
  721. }
  722. service := services.Services[0]
  723. var name string
  724. for _, t := range service.Tags {
  725. if *t.Key == compose.ServiceTag {
  726. name = aws.StringValue(t.Value)
  727. }
  728. }
  729. if name == "" {
  730. return compose.ServiceStatus{}, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  731. }
  732. targetGroupArns := []string{}
  733. for _, lb := range service.LoadBalancers {
  734. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  735. }
  736. // getURLwithPortMapping makes 2 queries
  737. // one to get the target groups and another for load balancers
  738. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  739. if err != nil {
  740. return compose.ServiceStatus{}, err
  741. }
  742. return compose.ServiceStatus{
  743. ID: aws.StringValue(service.ServiceName),
  744. Name: name,
  745. Replicas: int(aws.Int64Value(service.RunningCount)),
  746. Desired: int(aws.Int64Value(service.DesiredCount)),
  747. Publishers: loadBalancers,
  748. }, nil
  749. }
  750. func (s sdk) DescribeServiceTasks(ctx context.Context, cluster string, project string, service string) ([]compose.ContainerSummary, error) {
  751. var summary []compose.ContainerSummary
  752. familly := fmt.Sprintf("%s-%s", project, service)
  753. var token *string
  754. for {
  755. list, err := s.ECS.ListTasks(&ecs.ListTasksInput{
  756. Cluster: aws.String(cluster),
  757. Family: aws.String(familly),
  758. LaunchType: nil,
  759. MaxResults: nil,
  760. NextToken: token,
  761. })
  762. if err != nil {
  763. return nil, err
  764. }
  765. if len(list.TaskArns) == 0 {
  766. break
  767. }
  768. tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  769. Cluster: aws.String(cluster),
  770. Include: aws.StringSlice([]string{"TAGS"}),
  771. Tasks: list.TaskArns,
  772. })
  773. if err != nil {
  774. return nil, err
  775. }
  776. for _, t := range tasks.Tasks {
  777. var project string
  778. var service string
  779. for _, tag := range t.Tags {
  780. switch aws.StringValue(tag.Key) {
  781. case compose.ProjectTag:
  782. project = aws.StringValue(tag.Value)
  783. case compose.ServiceTag:
  784. service = aws.StringValue(tag.Value)
  785. }
  786. }
  787. id, err := arn.Parse(aws.StringValue(t.TaskArn))
  788. if err != nil {
  789. return nil, err
  790. }
  791. summary = append(summary, compose.ContainerSummary{
  792. ID: id.String(),
  793. Name: id.Resource,
  794. Project: project,
  795. Service: service,
  796. State: strings.Title(strings.ToLower(aws.StringValue(t.LastStatus))),
  797. })
  798. }
  799. if list.NextToken == token {
  800. break
  801. }
  802. token = list.NextToken
  803. }
  804. return summary, nil
  805. }
  806. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  807. if len(targetGroupArns) == 0 {
  808. return nil, nil
  809. }
  810. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  811. TargetGroupArns: aws.StringSlice(targetGroupArns),
  812. })
  813. if err != nil {
  814. return nil, err
  815. }
  816. lbarns := []*string{}
  817. for _, tg := range groups.TargetGroups {
  818. lbarns = append(lbarns, tg.LoadBalancerArns...)
  819. }
  820. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  821. LoadBalancerArns: lbarns,
  822. })
  823. if err != nil {
  824. return nil, err
  825. }
  826. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  827. if aws.StringValue(arn) == "" {
  828. // load balancer arn is nil/""
  829. return nil
  830. }
  831. for _, lb := range lbs {
  832. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  833. return lb
  834. }
  835. }
  836. return nil
  837. }
  838. loadBalancers := []compose.PortPublisher{}
  839. for _, tg := range groups.TargetGroups {
  840. for _, lbarn := range tg.LoadBalancerArns {
  841. lb := filterLB(lbarn, lbs.LoadBalancers)
  842. if lb == nil {
  843. continue
  844. }
  845. loadBalancers = append(loadBalancers, compose.PortPublisher{
  846. URL: fmt.Sprintf("%s:%d", aws.StringValue(lb.DNSName), aws.Int64Value(tg.Port)),
  847. TargetPort: int(aws.Int64Value(tg.Port)),
  848. PublishedPort: int(aws.Int64Value(tg.Port)),
  849. Protocol: strings.ToLower(aws.StringValue(tg.Protocol)),
  850. })
  851. }
  852. }
  853. return loadBalancers, nil
  854. }
  855. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  856. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  857. Cluster: aws.String(cluster),
  858. Family: aws.String(family),
  859. })
  860. if err != nil {
  861. return nil, err
  862. }
  863. arns := []string{}
  864. for _, arn := range tasks.TaskArns {
  865. arns = append(arns, *arn)
  866. }
  867. return arns, nil
  868. }
  869. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  870. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  871. NetworkInterfaceIds: aws.StringSlice(interfaces),
  872. })
  873. if err != nil {
  874. return nil, err
  875. }
  876. publicIPs := map[string]string{}
  877. for _, interf := range desc.NetworkInterfaces {
  878. if interf.Association != nil {
  879. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  880. }
  881. }
  882. return publicIPs, nil
  883. }
  884. func (s sdk) ResolveLoadBalancer(ctx context.Context, nameOrarn string) (awsResource, string, error) {
  885. logrus.Debug("Check if LoadBalancer exists: ", nameOrarn)
  886. var arns []*string
  887. var names []*string
  888. if arn.IsARN(nameOrarn) {
  889. arns = append(arns, aws.String(nameOrarn))
  890. } else {
  891. names = append(names, aws.String(nameOrarn))
  892. }
  893. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  894. LoadBalancerArns: arns,
  895. Names: names,
  896. })
  897. if err != nil {
  898. return nil, "", err
  899. }
  900. if len(lbs.LoadBalancers) == 0 {
  901. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "load balancer %q does not exist", nameOrarn)
  902. }
  903. it := lbs.LoadBalancers[0]
  904. return existingAWSResource{
  905. arn: aws.StringValue(it.LoadBalancerArn),
  906. id: aws.StringValue(it.LoadBalancerName),
  907. }, aws.StringValue(it.Type), nil
  908. }
  909. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  910. logrus.Debug("Retrieve load balancer URL: ", arn)
  911. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  912. LoadBalancerArns: []*string{aws.String(arn)},
  913. })
  914. if err != nil {
  915. return "", err
  916. }
  917. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  918. if dnsName == "" {
  919. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  920. }
  921. return dnsName, nil
  922. }
  923. func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
  924. parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  925. Name: aws.String(name),
  926. })
  927. if err != nil {
  928. return "", err
  929. }
  930. value := *parameter.Parameter.Value
  931. var ami struct {
  932. SchemaVersion int `json:"schema_version"`
  933. ImageName string `json:"image_name"`
  934. ImageID string `json:"image_id"`
  935. OS string `json:"os"`
  936. ECSRuntimeVersion string `json:"ecs_runtime_verion"`
  937. ECSAgentVersion string `json:"ecs_agent_version"`
  938. }
  939. err = json.Unmarshal([]byte(value), &ami)
  940. if err != nil {
  941. return "", err
  942. }
  943. return ami.ImageID, nil
  944. }
  945. func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
  946. desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
  947. GroupIds: aws.StringSlice([]string{sg}),
  948. })
  949. if err != nil {
  950. return false, err
  951. }
  952. return len(desc.SecurityGroups) > 0, nil
  953. }
  954. func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
  955. _, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
  956. CapacityProvider: aws.String(arn),
  957. })
  958. return err
  959. }
  960. func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
  961. _, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
  962. AutoScalingGroupName: aws.String(arn),
  963. ForceDelete: aws.Bool(true),
  964. })
  965. return err
  966. }
  967. func (s sdk) ResolveFileSystem(ctx context.Context, id string) (awsResource, error) {
  968. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  969. FileSystemId: aws.String(id),
  970. })
  971. if err != nil {
  972. return nil, err
  973. }
  974. if len(desc.FileSystems) == 0 {
  975. return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", id)
  976. }
  977. it := desc.FileSystems[0]
  978. return existingAWSResource{
  979. arn: aws.StringValue(it.FileSystemArn),
  980. id: aws.StringValue(it.FileSystemId),
  981. }, nil
  982. }
  983. func (s sdk) ListFileSystems(ctx context.Context, tags map[string]string) ([]awsResource, error) {
  984. var results []awsResource
  985. var token *string
  986. for {
  987. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  988. Marker: token,
  989. })
  990. if err != nil {
  991. return nil, err
  992. }
  993. for _, filesystem := range desc.FileSystems {
  994. if containsAll(filesystem.Tags, tags) {
  995. results = append(results, existingAWSResource{
  996. arn: aws.StringValue(filesystem.FileSystemArn),
  997. id: aws.StringValue(filesystem.FileSystemId),
  998. })
  999. }
  1000. }
  1001. if desc.NextMarker == token {
  1002. return results, nil
  1003. }
  1004. token = desc.NextMarker
  1005. }
  1006. }
  1007. func containsAll(tags []*efs.Tag, required map[string]string) bool {
  1008. TAGS:
  1009. for key, value := range required {
  1010. for _, t := range tags {
  1011. if aws.StringValue(t.Key) == key && aws.StringValue(t.Value) == value {
  1012. continue TAGS
  1013. }
  1014. }
  1015. return false
  1016. }
  1017. return true
  1018. }
  1019. func (s sdk) CreateFileSystem(ctx context.Context, tags map[string]string, options VolumeCreateOptions) (awsResource, error) {
  1020. var efsTags []*efs.Tag
  1021. for k, v := range tags {
  1022. efsTags = append(efsTags, &efs.Tag{
  1023. Key: aws.String(k),
  1024. Value: aws.String(v),
  1025. })
  1026. }
  1027. var (
  1028. k *string
  1029. p *string
  1030. f *float64
  1031. t *string
  1032. )
  1033. if options.ProvisionedThroughputInMibps > 1 {
  1034. f = aws.Float64(options.ProvisionedThroughputInMibps)
  1035. }
  1036. if options.KmsKeyID != "" {
  1037. k = aws.String(options.KmsKeyID)
  1038. }
  1039. if options.PerformanceMode != "" {
  1040. p = aws.String(options.PerformanceMode)
  1041. }
  1042. if options.ThroughputMode != "" {
  1043. t = aws.String(options.ThroughputMode)
  1044. }
  1045. res, err := s.EFS.CreateFileSystemWithContext(ctx, &efs.CreateFileSystemInput{
  1046. Encrypted: aws.Bool(true),
  1047. KmsKeyId: k,
  1048. PerformanceMode: p,
  1049. ProvisionedThroughputInMibps: f,
  1050. ThroughputMode: t,
  1051. Tags: efsTags,
  1052. })
  1053. if err != nil {
  1054. return nil, err
  1055. }
  1056. return existingAWSResource{
  1057. id: aws.StringValue(res.FileSystemId),
  1058. arn: aws.StringValue(res.FileSystemArn),
  1059. }, nil
  1060. }
  1061. func (s sdk) DeleteFileSystem(ctx context.Context, id string) error {
  1062. _, err := s.EFS.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{
  1063. FileSystemId: aws.String(id),
  1064. })
  1065. return err
  1066. }