sdk.go 34 KB

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