sdk.go 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  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(ctx context.Context, name string, url string) (string, error)
  218. func (s sdk) withTemplate(ctx context.Context, name string, template []byte, region string, fn uploadedTemplateFunc) (string, error) {
  219. logrus.Debug("Create s3 bucket to store cloudformation template")
  220. var configuration *s3.CreateBucketConfiguration
  221. if region != "us-east-1" {
  222. configuration = &s3.CreateBucketConfiguration{
  223. LocationConstraint: aws.String(region),
  224. }
  225. }
  226. _, err := s.S3.CreateBucket(&s3.CreateBucketInput{
  227. Bucket: aws.String("com.docker.compose." + region),
  228. CreateBucketConfiguration: configuration,
  229. })
  230. if err != nil {
  231. ae, ok := err.(awserr.Error)
  232. if !ok {
  233. return "", err
  234. }
  235. if ae.Code() != s3.ErrCodeBucketAlreadyOwnedByYou {
  236. return "", err
  237. }
  238. }
  239. key, err := uuid.GenerateUUID()
  240. if err != nil {
  241. return "", err
  242. }
  243. upload, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{
  244. Key: aws.String(key),
  245. Body: bytes.NewReader(template),
  246. Bucket: aws.String("com.docker.compose." + region),
  247. ContentType: aws.String("application/json"),
  248. Tagging: aws.String(name),
  249. })
  250. if err != nil {
  251. return "", err
  252. }
  253. defer s.S3.DeleteObjects(&s3.DeleteObjectsInput{ //nolint: errcheck
  254. Bucket: aws.String("com.docker.compose"),
  255. Delete: &s3.Delete{
  256. Objects: []*s3.ObjectIdentifier{
  257. {
  258. Key: aws.String(key),
  259. VersionId: upload.VersionID,
  260. },
  261. },
  262. },
  263. })
  264. return fn(ctx, name, upload.Location)
  265. }
  266. func (s sdk) CreateStack(ctx context.Context, name string, region string, template []byte) error {
  267. logrus.Debug("Create CloudFormation stack")
  268. stackID, err := s.withTemplate(ctx, name, template, region, func(ctx context.Context, name string, url string) (string, error) {
  269. stack, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  270. OnFailure: aws.String("DELETE"),
  271. StackName: aws.String(name),
  272. TemplateURL: aws.String(url),
  273. TimeoutInMinutes: nil,
  274. Capabilities: []*string{
  275. aws.String(cloudformation.CapabilityCapabilityIam),
  276. },
  277. Tags: []*cloudformation.Tag{
  278. {
  279. Key: aws.String(compose.ProjectTag),
  280. Value: aws.String(name),
  281. },
  282. },
  283. })
  284. if err != nil {
  285. return "", err
  286. }
  287. return aws.StringValue(stack.StackId), nil
  288. })
  289. logrus.Debugf("Stack %s created", stackID)
  290. return err
  291. }
  292. func (s sdk) CreateChangeSet(ctx context.Context, name string, region string, template []byte) (string, error) {
  293. logrus.Debug("Create CloudFormation Changeset")
  294. changeset, err := s.withTemplate(ctx, name, template, region, func(ctx context.Context, name string, url string) (string, error) {
  295. update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
  296. changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
  297. ChangeSetName: aws.String(update),
  298. ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
  299. StackName: aws.String(name),
  300. TemplateBody: aws.String(string(template)),
  301. Capabilities: []*string{
  302. aws.String(cloudformation.CapabilityCapabilityIam),
  303. },
  304. })
  305. if err != nil {
  306. return "", err
  307. }
  308. return aws.StringValue(changeset.Id), err
  309. })
  310. if err != nil {
  311. return "", err
  312. }
  313. // we have to WaitUntilChangeSetCreateComplete even this in fail with error `ResourceNotReady`
  314. // so that we can invoke DescribeChangeSet to check status, and then we can know about the actual creation failure cause.
  315. s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ // nolint:errcheck
  316. ChangeSetName: aws.String(changeset),
  317. })
  318. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  319. ChangeSetName: aws.String(update),
  320. StackName: aws.String(name),
  321. })
  322. if aws.StringValue(desc.Status) == "FAILED" {
  323. return changeset, fmt.Errorf(aws.StringValue(desc.StatusReason))
  324. }
  325. return changeset, err
  326. }
  327. func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
  328. desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
  329. ChangeSetName: aws.String(changeset),
  330. })
  331. if err != nil {
  332. return err
  333. }
  334. if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") {
  335. return nil
  336. }
  337. _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{
  338. ChangeSetName: aws.String(changeset),
  339. })
  340. return err
  341. }
  342. const (
  343. stackCreate = iota
  344. stackUpdate
  345. stackDelete
  346. )
  347. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  348. input := &cloudformation.DescribeStacksInput{
  349. StackName: aws.String(name),
  350. }
  351. switch operation {
  352. case stackCreate:
  353. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  354. case stackDelete:
  355. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  356. default:
  357. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  358. }
  359. }
  360. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  361. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  362. StackName: aws.String(name),
  363. })
  364. if err != nil {
  365. return "", err
  366. }
  367. return *stacks.Stacks[0].StackId, nil
  368. }
  369. func (s sdk) ListStacks(ctx context.Context, name string) ([]compose.Stack, error) {
  370. params := cloudformation.DescribeStacksInput{}
  371. if name != "" {
  372. params.StackName = &name
  373. }
  374. cfStacks, err := s.CF.DescribeStacksWithContext(ctx, &params)
  375. if err != nil {
  376. return nil, err
  377. }
  378. stacks := []compose.Stack{}
  379. for _, stack := range cfStacks.Stacks {
  380. for _, t := range stack.Tags {
  381. if *t.Key == compose.ProjectTag {
  382. status := compose.RUNNING
  383. switch aws.StringValue(stack.StackStatus) {
  384. case "CREATE_IN_PROGRESS":
  385. status = compose.STARTING
  386. case "DELETE_IN_PROGRESS":
  387. status = compose.REMOVING
  388. case "UPDATE_IN_PROGRESS":
  389. status = compose.UPDATING
  390. default:
  391. }
  392. stacks = append(stacks, compose.Stack{
  393. ID: aws.StringValue(stack.StackId),
  394. Name: aws.StringValue(stack.StackName),
  395. Status: status,
  396. })
  397. break
  398. }
  399. }
  400. }
  401. return stacks, nil
  402. }
  403. func (s sdk) GetStackClusterID(ctx context.Context, stack string) (string, error) {
  404. // Note: could use DescribeStackResource but we only can detect `does not exist` case by matching string error message
  405. resources, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  406. StackName: aws.String(stack),
  407. })
  408. if err != nil {
  409. return "", err
  410. }
  411. for _, r := range resources.StackResourceSummaries {
  412. if aws.StringValue(r.ResourceType) == "AWS::ECS::Cluster" {
  413. return aws.StringValue(r.PhysicalResourceId), nil
  414. }
  415. }
  416. // stack is using user-provided cluster
  417. res, err := s.CF.GetTemplateSummaryWithContext(ctx, &cloudformation.GetTemplateSummaryInput{
  418. StackName: aws.String(stack),
  419. })
  420. if err != nil {
  421. return "", err
  422. }
  423. c := aws.StringValue(res.Metadata)
  424. var m templateMetadata
  425. err = json.Unmarshal([]byte(c), &m)
  426. if err != nil {
  427. return "", err
  428. }
  429. if m.Cluster == "" {
  430. return "", errors.Wrap(errdefs.ErrNotFound, "CloudFormation is missing cluster metadata")
  431. }
  432. return m.Cluster, nil
  433. }
  434. type templateMetadata struct {
  435. Cluster string `json:",omitempty"`
  436. }
  437. func (s sdk) GetServiceTaskDefinition(ctx context.Context, cluster string, serviceArns []string) (map[string]string, error) {
  438. defs := map[string]string{}
  439. svc := []*string{}
  440. for _, s := range serviceArns {
  441. svc = append(svc, aws.String(s))
  442. }
  443. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  444. Cluster: aws.String(cluster),
  445. Services: svc,
  446. })
  447. if err != nil {
  448. return nil, err
  449. }
  450. for _, s := range services.Services {
  451. defs[aws.StringValue(s.ServiceArn)] = aws.StringValue(s.TaskDefinition)
  452. }
  453. return defs, nil
  454. }
  455. func (s sdk) ListStackServices(ctx context.Context, stack string) ([]string, error) {
  456. arns := []string{}
  457. var nextToken *string
  458. for {
  459. response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  460. StackName: aws.String(stack),
  461. NextToken: nextToken,
  462. })
  463. if err != nil {
  464. return nil, err
  465. }
  466. for _, r := range response.StackResourceSummaries {
  467. if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
  468. if r.PhysicalResourceId != nil {
  469. arns = append(arns, aws.StringValue(r.PhysicalResourceId))
  470. }
  471. }
  472. }
  473. nextToken = response.NextToken
  474. if nextToken == nil {
  475. break
  476. }
  477. }
  478. return arns, nil
  479. }
  480. func (s sdk) GetServiceTasks(ctx context.Context, cluster string, service string, stopped bool) ([]*ecs.Task, error) {
  481. state := "RUNNING"
  482. if stopped {
  483. state = "STOPPED"
  484. }
  485. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  486. Cluster: aws.String(cluster),
  487. ServiceName: aws.String(service),
  488. DesiredStatus: aws.String(state),
  489. })
  490. if err != nil {
  491. return nil, err
  492. }
  493. if len(tasks.TaskArns) > 0 {
  494. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  495. Cluster: aws.String(cluster),
  496. Tasks: tasks.TaskArns,
  497. })
  498. if err != nil {
  499. return nil, err
  500. }
  501. return taskDescriptions.Tasks, nil
  502. }
  503. return nil, nil
  504. }
  505. func (s sdk) GetTaskStoppedReason(ctx context.Context, cluster string, taskArn string) (string, error) {
  506. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  507. Cluster: aws.String(cluster),
  508. Tasks: []*string{aws.String(taskArn)},
  509. })
  510. if err != nil {
  511. return "", err
  512. }
  513. if len(taskDescriptions.Tasks) == 0 {
  514. return "", nil
  515. }
  516. task := taskDescriptions.Tasks[0]
  517. return fmt.Sprintf(
  518. "%s: %s",
  519. aws.StringValue(task.StopCode),
  520. aws.StringValue(task.StoppedReason)), nil
  521. }
  522. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  523. // Fixme implement Paginator on Events and return as a chan(events)
  524. events := []*cloudformation.StackEvent{}
  525. var nextToken *string
  526. for {
  527. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  528. StackName: aws.String(stackID),
  529. NextToken: nextToken,
  530. })
  531. if err != nil {
  532. return nil, err
  533. }
  534. events = append(events, resp.StackEvents...)
  535. if resp.NextToken == nil {
  536. return events, nil
  537. }
  538. nextToken = resp.NextToken
  539. }
  540. }
  541. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  542. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  543. NextToken: nil,
  544. StackName: aws.String(name),
  545. })
  546. if err != nil {
  547. return nil, err
  548. }
  549. parameters := map[string]string{}
  550. for _, parameter := range st.Stacks[0].Parameters {
  551. parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue)
  552. }
  553. return parameters, nil
  554. }
  555. type stackResource struct {
  556. LogicalID string
  557. Type string
  558. ARN string
  559. Status string
  560. }
  561. type stackResourceFn func(r stackResource) error
  562. type stackResources []stackResource
  563. func (resources stackResources) apply(awsType string, fn stackResourceFn) error {
  564. var errs *multierror.Error
  565. for _, r := range resources {
  566. if r.Type == awsType {
  567. err := fn(r)
  568. if err != nil {
  569. errs = multierror.Append(err)
  570. }
  571. }
  572. }
  573. return errs.ErrorOrNil()
  574. }
  575. func (s sdk) ListStackResources(ctx context.Context, name string) (stackResources, error) {
  576. // FIXME handle pagination
  577. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  578. StackName: aws.String(name),
  579. })
  580. if err != nil {
  581. return nil, err
  582. }
  583. resources := stackResources{}
  584. for _, r := range res.StackResourceSummaries {
  585. resources = append(resources, stackResource{
  586. LogicalID: aws.StringValue(r.LogicalResourceId),
  587. Type: aws.StringValue(r.ResourceType),
  588. ARN: aws.StringValue(r.PhysicalResourceId),
  589. Status: aws.StringValue(r.ResourceStatus),
  590. })
  591. }
  592. return resources, nil
  593. }
  594. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  595. logrus.Debug("Delete CloudFormation stack")
  596. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  597. StackName: aws.String(name),
  598. })
  599. return err
  600. }
  601. func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) {
  602. logrus.Debug("Create secret " + secret.Name)
  603. var tags []*secretsmanager.Tag
  604. for k, v := range secret.Labels {
  605. tags = []*secretsmanager.Tag{
  606. {
  607. Key: aws.String(k),
  608. Value: aws.String(v),
  609. },
  610. }
  611. }
  612. // store the secret content as string
  613. content := string(secret.GetContent())
  614. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  615. Name: &secret.Name,
  616. SecretString: &content,
  617. Tags: tags,
  618. })
  619. if err != nil {
  620. return "", err
  621. }
  622. return aws.StringValue(response.ARN), nil
  623. }
  624. func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) {
  625. logrus.Debug("Inspect secret " + id)
  626. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  627. if err != nil {
  628. return secrets.Secret{}, err
  629. }
  630. tags := map[string]string{}
  631. for _, tag := range response.Tags {
  632. tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
  633. }
  634. secret := secrets.Secret{
  635. ID: aws.StringValue(response.ARN),
  636. Name: aws.StringValue(response.Name),
  637. Labels: tags,
  638. }
  639. return secret, nil
  640. }
  641. func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) {
  642. logrus.Debug("List secrets ...")
  643. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  644. if err != nil {
  645. return nil, err
  646. }
  647. var ls []secrets.Secret
  648. for _, sec := range response.SecretList {
  649. tags := map[string]string{}
  650. for _, tag := range sec.Tags {
  651. tags[*tag.Key] = *tag.Value
  652. }
  653. ls = append(ls, secrets.Secret{
  654. ID: *sec.ARN,
  655. Name: *sec.Name,
  656. Labels: tags,
  657. })
  658. }
  659. return ls, nil
  660. }
  661. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  662. logrus.Debug("List secrets ...")
  663. force := !recover
  664. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  665. return err
  666. }
  667. func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error {
  668. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  669. var startTime int64
  670. for {
  671. select {
  672. case <-ctx.Done():
  673. return nil
  674. default:
  675. var hasMore = true
  676. var token *string
  677. for hasMore {
  678. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  679. LogGroupName: aws.String(logGroup),
  680. NextToken: token,
  681. StartTime: aws.Int64(startTime),
  682. })
  683. if err != nil {
  684. return err
  685. }
  686. if events.NextToken == nil {
  687. hasMore = false
  688. } else {
  689. token = events.NextToken
  690. }
  691. for _, event := range events.Events {
  692. p := strings.Split(aws.StringValue(event.LogStreamName), "/")
  693. consumer(p[1], p[2], aws.StringValue(event.Message))
  694. startTime = *event.IngestionTime
  695. }
  696. }
  697. }
  698. time.Sleep(500 * time.Millisecond)
  699. }
  700. }
  701. func (s sdk) DescribeService(ctx context.Context, cluster string, arn string) (compose.ServiceStatus, error) {
  702. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  703. Cluster: aws.String(cluster),
  704. Services: []*string{aws.String(arn)},
  705. Include: aws.StringSlice([]string{"TAGS"}),
  706. })
  707. if err != nil {
  708. return compose.ServiceStatus{}, err
  709. }
  710. for _, f := range services.Failures {
  711. return compose.ServiceStatus{}, errors.Wrapf(errdefs.ErrNotFound, "can't get service status %s: %s", aws.StringValue(f.Detail), aws.StringValue(f.Reason))
  712. }
  713. service := services.Services[0]
  714. var name string
  715. for _, t := range service.Tags {
  716. if *t.Key == compose.ServiceTag {
  717. name = aws.StringValue(t.Value)
  718. }
  719. }
  720. if name == "" {
  721. return compose.ServiceStatus{}, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  722. }
  723. targetGroupArns := []string{}
  724. for _, lb := range service.LoadBalancers {
  725. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  726. }
  727. // getURLwithPortMapping makes 2 queries
  728. // one to get the target groups and another for load balancers
  729. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  730. if err != nil {
  731. return compose.ServiceStatus{}, err
  732. }
  733. return compose.ServiceStatus{
  734. ID: aws.StringValue(service.ServiceName),
  735. Name: name,
  736. Replicas: int(aws.Int64Value(service.RunningCount)),
  737. Desired: int(aws.Int64Value(service.DesiredCount)),
  738. Publishers: loadBalancers,
  739. }, nil
  740. }
  741. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  742. if len(targetGroupArns) == 0 {
  743. return nil, nil
  744. }
  745. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  746. TargetGroupArns: aws.StringSlice(targetGroupArns),
  747. })
  748. if err != nil {
  749. return nil, err
  750. }
  751. lbarns := []*string{}
  752. for _, tg := range groups.TargetGroups {
  753. lbarns = append(lbarns, tg.LoadBalancerArns...)
  754. }
  755. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  756. LoadBalancerArns: lbarns,
  757. })
  758. if err != nil {
  759. return nil, err
  760. }
  761. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  762. if aws.StringValue(arn) == "" {
  763. // load balancer arn is nil/""
  764. return nil
  765. }
  766. for _, lb := range lbs {
  767. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  768. return lb
  769. }
  770. }
  771. return nil
  772. }
  773. loadBalancers := []compose.PortPublisher{}
  774. for _, tg := range groups.TargetGroups {
  775. for _, lbarn := range tg.LoadBalancerArns {
  776. lb := filterLB(lbarn, lbs.LoadBalancers)
  777. if lb == nil {
  778. continue
  779. }
  780. loadBalancers = append(loadBalancers, compose.PortPublisher{
  781. URL: aws.StringValue(lb.DNSName),
  782. TargetPort: int(aws.Int64Value(tg.Port)),
  783. PublishedPort: int(aws.Int64Value(tg.Port)),
  784. Protocol: aws.StringValue(tg.Protocol),
  785. })
  786. }
  787. }
  788. return loadBalancers, nil
  789. }
  790. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  791. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  792. Cluster: aws.String(cluster),
  793. Family: aws.String(family),
  794. })
  795. if err != nil {
  796. return nil, err
  797. }
  798. arns := []string{}
  799. for _, arn := range tasks.TaskArns {
  800. arns = append(arns, *arn)
  801. }
  802. return arns, nil
  803. }
  804. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  805. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  806. NetworkInterfaceIds: aws.StringSlice(interfaces),
  807. })
  808. if err != nil {
  809. return nil, err
  810. }
  811. publicIPs := map[string]string{}
  812. for _, interf := range desc.NetworkInterfaces {
  813. if interf.Association != nil {
  814. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  815. }
  816. }
  817. return publicIPs, nil
  818. }
  819. func (s sdk) ResolveLoadBalancer(ctx context.Context, nameOrarn string) (awsResource, string, error) {
  820. logrus.Debug("Check if LoadBalancer exists: ", nameOrarn)
  821. var arns []*string
  822. var names []*string
  823. if arn.IsARN(nameOrarn) {
  824. arns = append(arns, aws.String(nameOrarn))
  825. } else {
  826. names = append(names, aws.String(nameOrarn))
  827. }
  828. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  829. LoadBalancerArns: arns,
  830. Names: names,
  831. })
  832. if err != nil {
  833. return nil, "", err
  834. }
  835. if len(lbs.LoadBalancers) == 0 {
  836. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "load balancer %q does not exist", nameOrarn)
  837. }
  838. it := lbs.LoadBalancers[0]
  839. return existingAWSResource{
  840. arn: aws.StringValue(it.LoadBalancerArn),
  841. id: aws.StringValue(it.LoadBalancerName),
  842. }, aws.StringValue(it.Type), nil
  843. }
  844. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  845. logrus.Debug("Retrieve load balancer URL: ", arn)
  846. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  847. LoadBalancerArns: []*string{aws.String(arn)},
  848. })
  849. if err != nil {
  850. return "", err
  851. }
  852. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  853. if dnsName == "" {
  854. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  855. }
  856. return dnsName, nil
  857. }
  858. func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
  859. parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  860. Name: aws.String(name),
  861. })
  862. if err != nil {
  863. return "", err
  864. }
  865. value := *parameter.Parameter.Value
  866. var ami struct {
  867. SchemaVersion int `json:"schema_version"`
  868. ImageName string `json:"image_name"`
  869. ImageID string `json:"image_id"`
  870. OS string `json:"os"`
  871. ECSRuntimeVersion string `json:"ecs_runtime_verion"`
  872. ECSAgentVersion string `json:"ecs_agent_version"`
  873. }
  874. err = json.Unmarshal([]byte(value), &ami)
  875. if err != nil {
  876. return "", err
  877. }
  878. return ami.ImageID, nil
  879. }
  880. func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
  881. desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
  882. GroupIds: aws.StringSlice([]string{sg}),
  883. })
  884. if err != nil {
  885. return false, err
  886. }
  887. return len(desc.SecurityGroups) > 0, nil
  888. }
  889. func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
  890. _, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
  891. CapacityProvider: aws.String(arn),
  892. })
  893. return err
  894. }
  895. func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
  896. _, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
  897. AutoScalingGroupName: aws.String(arn),
  898. ForceDelete: aws.Bool(true),
  899. })
  900. return err
  901. }
  902. func (s sdk) ResolveFileSystem(ctx context.Context, id string) (awsResource, error) {
  903. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  904. FileSystemId: aws.String(id),
  905. })
  906. if err != nil {
  907. return nil, err
  908. }
  909. if len(desc.FileSystems) == 0 {
  910. return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", id)
  911. }
  912. it := desc.FileSystems[0]
  913. return existingAWSResource{
  914. arn: aws.StringValue(it.FileSystemArn),
  915. id: aws.StringValue(it.FileSystemId),
  916. }, nil
  917. }
  918. func (s sdk) ListFileSystems(ctx context.Context, tags map[string]string) ([]awsResource, error) {
  919. var results []awsResource
  920. var token *string
  921. for {
  922. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  923. Marker: token,
  924. })
  925. if err != nil {
  926. return nil, err
  927. }
  928. for _, filesystem := range desc.FileSystems {
  929. if containsAll(filesystem.Tags, tags) {
  930. results = append(results, existingAWSResource{
  931. arn: aws.StringValue(filesystem.FileSystemArn),
  932. id: aws.StringValue(filesystem.FileSystemId),
  933. })
  934. }
  935. }
  936. if desc.NextMarker == token {
  937. return results, nil
  938. }
  939. token = desc.NextMarker
  940. }
  941. }
  942. func containsAll(tags []*efs.Tag, required map[string]string) bool {
  943. TAGS:
  944. for key, value := range required {
  945. for _, t := range tags {
  946. if aws.StringValue(t.Key) == key && aws.StringValue(t.Value) == value {
  947. continue TAGS
  948. }
  949. }
  950. return false
  951. }
  952. return true
  953. }
  954. func (s sdk) CreateFileSystem(ctx context.Context, tags map[string]string, options VolumeCreateOptions) (awsResource, error) {
  955. var efsTags []*efs.Tag
  956. for k, v := range tags {
  957. efsTags = append(efsTags, &efs.Tag{
  958. Key: aws.String(k),
  959. Value: aws.String(v),
  960. })
  961. }
  962. var (
  963. k *string
  964. p *string
  965. f *float64
  966. t *string
  967. )
  968. if options.ProvisionedThroughputInMibps > 1 {
  969. f = aws.Float64(options.ProvisionedThroughputInMibps)
  970. }
  971. if options.KmsKeyID != "" {
  972. k = aws.String(options.KmsKeyID)
  973. }
  974. if options.PerformanceMode != "" {
  975. p = aws.String(options.PerformanceMode)
  976. }
  977. if options.ThroughputMode != "" {
  978. t = aws.String(options.ThroughputMode)
  979. }
  980. res, err := s.EFS.CreateFileSystemWithContext(ctx, &efs.CreateFileSystemInput{
  981. Encrypted: aws.Bool(true),
  982. KmsKeyId: k,
  983. PerformanceMode: p,
  984. ProvisionedThroughputInMibps: f,
  985. ThroughputMode: t,
  986. Tags: efsTags,
  987. })
  988. if err != nil {
  989. return nil, err
  990. }
  991. return existingAWSResource{
  992. id: aws.StringValue(res.FileSystemId),
  993. arn: aws.StringValue(res.FileSystemArn),
  994. }, nil
  995. }
  996. func (s sdk) DeleteFileSystem(ctx context.Context, id string) error {
  997. _, err := s.EFS.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{
  998. FileSystemId: aws.String(id),
  999. })
  1000. return err
  1001. }