sdk.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101
  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) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  751. if len(targetGroupArns) == 0 {
  752. return nil, nil
  753. }
  754. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  755. TargetGroupArns: aws.StringSlice(targetGroupArns),
  756. })
  757. if err != nil {
  758. return nil, err
  759. }
  760. lbarns := []*string{}
  761. for _, tg := range groups.TargetGroups {
  762. lbarns = append(lbarns, tg.LoadBalancerArns...)
  763. }
  764. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  765. LoadBalancerArns: lbarns,
  766. })
  767. if err != nil {
  768. return nil, err
  769. }
  770. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  771. if aws.StringValue(arn) == "" {
  772. // load balancer arn is nil/""
  773. return nil
  774. }
  775. for _, lb := range lbs {
  776. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  777. return lb
  778. }
  779. }
  780. return nil
  781. }
  782. loadBalancers := []compose.PortPublisher{}
  783. for _, tg := range groups.TargetGroups {
  784. for _, lbarn := range tg.LoadBalancerArns {
  785. lb := filterLB(lbarn, lbs.LoadBalancers)
  786. if lb == nil {
  787. continue
  788. }
  789. loadBalancers = append(loadBalancers, compose.PortPublisher{
  790. URL: aws.StringValue(lb.DNSName),
  791. TargetPort: int(aws.Int64Value(tg.Port)),
  792. PublishedPort: int(aws.Int64Value(tg.Port)),
  793. Protocol: aws.StringValue(tg.Protocol),
  794. })
  795. }
  796. }
  797. return loadBalancers, nil
  798. }
  799. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  800. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  801. Cluster: aws.String(cluster),
  802. Family: aws.String(family),
  803. })
  804. if err != nil {
  805. return nil, err
  806. }
  807. arns := []string{}
  808. for _, arn := range tasks.TaskArns {
  809. arns = append(arns, *arn)
  810. }
  811. return arns, nil
  812. }
  813. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  814. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  815. NetworkInterfaceIds: aws.StringSlice(interfaces),
  816. })
  817. if err != nil {
  818. return nil, err
  819. }
  820. publicIPs := map[string]string{}
  821. for _, interf := range desc.NetworkInterfaces {
  822. if interf.Association != nil {
  823. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  824. }
  825. }
  826. return publicIPs, nil
  827. }
  828. func (s sdk) ResolveLoadBalancer(ctx context.Context, nameOrarn string) (awsResource, string, error) {
  829. logrus.Debug("Check if LoadBalancer exists: ", nameOrarn)
  830. var arns []*string
  831. var names []*string
  832. if arn.IsARN(nameOrarn) {
  833. arns = append(arns, aws.String(nameOrarn))
  834. } else {
  835. names = append(names, aws.String(nameOrarn))
  836. }
  837. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  838. LoadBalancerArns: arns,
  839. Names: names,
  840. })
  841. if err != nil {
  842. return nil, "", err
  843. }
  844. if len(lbs.LoadBalancers) == 0 {
  845. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "load balancer %q does not exist", nameOrarn)
  846. }
  847. it := lbs.LoadBalancers[0]
  848. return existingAWSResource{
  849. arn: aws.StringValue(it.LoadBalancerArn),
  850. id: aws.StringValue(it.LoadBalancerName),
  851. }, aws.StringValue(it.Type), nil
  852. }
  853. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  854. logrus.Debug("Retrieve load balancer URL: ", arn)
  855. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  856. LoadBalancerArns: []*string{aws.String(arn)},
  857. })
  858. if err != nil {
  859. return "", err
  860. }
  861. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  862. if dnsName == "" {
  863. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  864. }
  865. return dnsName, nil
  866. }
  867. func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
  868. parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  869. Name: aws.String(name),
  870. })
  871. if err != nil {
  872. return "", err
  873. }
  874. value := *parameter.Parameter.Value
  875. var ami struct {
  876. SchemaVersion int `json:"schema_version"`
  877. ImageName string `json:"image_name"`
  878. ImageID string `json:"image_id"`
  879. OS string `json:"os"`
  880. ECSRuntimeVersion string `json:"ecs_runtime_verion"`
  881. ECSAgentVersion string `json:"ecs_agent_version"`
  882. }
  883. err = json.Unmarshal([]byte(value), &ami)
  884. if err != nil {
  885. return "", err
  886. }
  887. return ami.ImageID, nil
  888. }
  889. func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
  890. desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
  891. GroupIds: aws.StringSlice([]string{sg}),
  892. })
  893. if err != nil {
  894. return false, err
  895. }
  896. return len(desc.SecurityGroups) > 0, nil
  897. }
  898. func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
  899. _, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
  900. CapacityProvider: aws.String(arn),
  901. })
  902. return err
  903. }
  904. func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
  905. _, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
  906. AutoScalingGroupName: aws.String(arn),
  907. ForceDelete: aws.Bool(true),
  908. })
  909. return err
  910. }
  911. func (s sdk) ResolveFileSystem(ctx context.Context, id string) (awsResource, error) {
  912. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  913. FileSystemId: aws.String(id),
  914. })
  915. if err != nil {
  916. return nil, err
  917. }
  918. if len(desc.FileSystems) == 0 {
  919. return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", id)
  920. }
  921. it := desc.FileSystems[0]
  922. return existingAWSResource{
  923. arn: aws.StringValue(it.FileSystemArn),
  924. id: aws.StringValue(it.FileSystemId),
  925. }, nil
  926. }
  927. func (s sdk) ListFileSystems(ctx context.Context, tags map[string]string) ([]awsResource, error) {
  928. var results []awsResource
  929. var token *string
  930. for {
  931. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  932. Marker: token,
  933. })
  934. if err != nil {
  935. return nil, err
  936. }
  937. for _, filesystem := range desc.FileSystems {
  938. if containsAll(filesystem.Tags, tags) {
  939. results = append(results, existingAWSResource{
  940. arn: aws.StringValue(filesystem.FileSystemArn),
  941. id: aws.StringValue(filesystem.FileSystemId),
  942. })
  943. }
  944. }
  945. if desc.NextMarker == token {
  946. return results, nil
  947. }
  948. token = desc.NextMarker
  949. }
  950. }
  951. func containsAll(tags []*efs.Tag, required map[string]string) bool {
  952. TAGS:
  953. for key, value := range required {
  954. for _, t := range tags {
  955. if aws.StringValue(t.Key) == key && aws.StringValue(t.Value) == value {
  956. continue TAGS
  957. }
  958. }
  959. return false
  960. }
  961. return true
  962. }
  963. func (s sdk) CreateFileSystem(ctx context.Context, tags map[string]string, options VolumeCreateOptions) (awsResource, error) {
  964. var efsTags []*efs.Tag
  965. for k, v := range tags {
  966. efsTags = append(efsTags, &efs.Tag{
  967. Key: aws.String(k),
  968. Value: aws.String(v),
  969. })
  970. }
  971. var (
  972. k *string
  973. p *string
  974. f *float64
  975. t *string
  976. )
  977. if options.ProvisionedThroughputInMibps > 1 {
  978. f = aws.Float64(options.ProvisionedThroughputInMibps)
  979. }
  980. if options.KmsKeyID != "" {
  981. k = aws.String(options.KmsKeyID)
  982. }
  983. if options.PerformanceMode != "" {
  984. p = aws.String(options.PerformanceMode)
  985. }
  986. if options.ThroughputMode != "" {
  987. t = aws.String(options.ThroughputMode)
  988. }
  989. res, err := s.EFS.CreateFileSystemWithContext(ctx, &efs.CreateFileSystemInput{
  990. Encrypted: aws.Bool(true),
  991. KmsKeyId: k,
  992. PerformanceMode: p,
  993. ProvisionedThroughputInMibps: f,
  994. ThroughputMode: t,
  995. Tags: efsTags,
  996. })
  997. if err != nil {
  998. return nil, err
  999. }
  1000. return existingAWSResource{
  1001. id: aws.StringValue(res.FileSystemId),
  1002. arn: aws.StringValue(res.FileSystemArn),
  1003. }, nil
  1004. }
  1005. func (s sdk) DeleteFileSystem(ctx context.Context, id string) error {
  1006. _, err := s.EFS.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{
  1007. FileSystemId: aws.String(id),
  1008. })
  1009. return err
  1010. }