sdk.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247
  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. var 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. var token *string
  414. var stacks []compose.Stack
  415. for {
  416. response, err := s.CF.DescribeStacksWithContext(ctx, &params)
  417. if err != nil {
  418. return nil, err
  419. }
  420. for _, stack := range response.Stacks {
  421. for _, t := range stack.Tags {
  422. if *t.Key == compose.ProjectTag {
  423. status := compose.RUNNING
  424. switch aws.StringValue(stack.StackStatus) {
  425. case "CREATE_IN_PROGRESS":
  426. status = compose.STARTING
  427. case "DELETE_IN_PROGRESS":
  428. status = compose.REMOVING
  429. case "UPDATE_IN_PROGRESS":
  430. status = compose.UPDATING
  431. default:
  432. }
  433. stacks = append(stacks, compose.Stack{
  434. ID: aws.StringValue(stack.StackId),
  435. Name: aws.StringValue(stack.StackName),
  436. Status: status,
  437. })
  438. break
  439. }
  440. }
  441. }
  442. if token == response.NextToken {
  443. return stacks, nil
  444. }
  445. token = response.NextToken
  446. }
  447. }
  448. func (s sdk) GetStackClusterID(ctx context.Context, stack string) (string, error) {
  449. // Note: could use DescribeStackResource but we only can detect `does not exist` case by matching string error message
  450. var token *string
  451. for {
  452. response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  453. StackName: aws.String(stack),
  454. })
  455. if err != nil {
  456. return "", err
  457. }
  458. for _, r := range response.StackResourceSummaries {
  459. if aws.StringValue(r.ResourceType) == "AWS::ECS::Cluster" {
  460. return aws.StringValue(r.PhysicalResourceId), nil
  461. }
  462. }
  463. if token == response.NextToken {
  464. break
  465. }
  466. token = response.NextToken
  467. }
  468. // stack is using user-provided cluster
  469. res, err := s.CF.GetTemplateSummaryWithContext(ctx, &cloudformation.GetTemplateSummaryInput{
  470. StackName: aws.String(stack),
  471. })
  472. if err != nil {
  473. return "", err
  474. }
  475. c := aws.StringValue(res.Metadata)
  476. var m templateMetadata
  477. err = json.Unmarshal([]byte(c), &m)
  478. if err != nil {
  479. return "", err
  480. }
  481. if m.Cluster == "" {
  482. return "", errors.Wrap(errdefs.ErrNotFound, "CloudFormation is missing cluster metadata")
  483. }
  484. return m.Cluster, nil
  485. }
  486. type templateMetadata struct {
  487. Cluster string `json:",omitempty"`
  488. }
  489. func (s sdk) GetServiceTaskDefinition(ctx context.Context, cluster string, serviceArns []string) (map[string]string, error) {
  490. defs := map[string]string{}
  491. svc := []*string{}
  492. for _, s := range serviceArns {
  493. svc = append(svc, aws.String(s))
  494. }
  495. for i := 0; i < len(svc); i += 10 {
  496. end := i + 10
  497. if end > len(svc) {
  498. end = len(svc)
  499. }
  500. chunk := svc[i:end]
  501. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  502. Cluster: aws.String(cluster),
  503. Services: chunk,
  504. })
  505. if err != nil {
  506. return nil, err
  507. }
  508. for _, s := range services.Services {
  509. defs[aws.StringValue(s.ServiceArn)] = aws.StringValue(s.TaskDefinition)
  510. }
  511. }
  512. return defs, nil
  513. }
  514. func (s sdk) ListStackServices(ctx context.Context, stack string) ([]string, error) {
  515. arns := []string{}
  516. var nextToken *string
  517. for {
  518. response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  519. StackName: aws.String(stack),
  520. NextToken: nextToken,
  521. })
  522. if err != nil {
  523. return nil, err
  524. }
  525. for _, r := range response.StackResourceSummaries {
  526. if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
  527. if r.PhysicalResourceId != nil {
  528. arns = append(arns, aws.StringValue(r.PhysicalResourceId))
  529. }
  530. }
  531. }
  532. nextToken = response.NextToken
  533. if nextToken == nil {
  534. break
  535. }
  536. }
  537. return arns, nil
  538. }
  539. func (s sdk) GetServiceTasks(ctx context.Context, cluster string, service string, stopped bool) ([]*ecs.Task, error) {
  540. state := "RUNNING"
  541. if stopped {
  542. state = "STOPPED"
  543. }
  544. var token *string
  545. var tasks []*ecs.Task
  546. for {
  547. response, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  548. Cluster: aws.String(cluster),
  549. ServiceName: aws.String(service),
  550. DesiredStatus: aws.String(state),
  551. })
  552. if err != nil {
  553. return nil, err
  554. }
  555. if len(response.TaskArns) > 0 {
  556. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  557. Cluster: aws.String(cluster),
  558. Tasks: response.TaskArns,
  559. })
  560. if err != nil {
  561. return nil, err
  562. }
  563. tasks = append(tasks, taskDescriptions.Tasks...)
  564. }
  565. if token == response.NextToken {
  566. return tasks, nil
  567. }
  568. token = response.NextToken
  569. }
  570. }
  571. func (s sdk) GetTaskStoppedReason(ctx context.Context, cluster string, taskArn string) (string, error) {
  572. taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  573. Cluster: aws.String(cluster),
  574. Tasks: []*string{aws.String(taskArn)},
  575. })
  576. if err != nil {
  577. return "", err
  578. }
  579. if len(taskDescriptions.Tasks) == 0 {
  580. return "", nil
  581. }
  582. task := taskDescriptions.Tasks[0]
  583. return fmt.Sprintf(
  584. "%s: %s",
  585. aws.StringValue(task.StopCode),
  586. aws.StringValue(task.StoppedReason)), nil
  587. }
  588. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  589. // Fixme implement Paginator on Events and return as a chan(events)
  590. events := []*cloudformation.StackEvent{}
  591. var nextToken *string
  592. for {
  593. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  594. StackName: aws.String(stackID),
  595. NextToken: nextToken,
  596. })
  597. if err != nil {
  598. return nil, err
  599. }
  600. events = append(events, resp.StackEvents...)
  601. if resp.NextToken == nil {
  602. return events, nil
  603. }
  604. nextToken = resp.NextToken
  605. }
  606. }
  607. func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) {
  608. st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  609. NextToken: nil,
  610. StackName: aws.String(name),
  611. })
  612. if err != nil {
  613. return nil, err
  614. }
  615. parameters := map[string]string{}
  616. for _, parameter := range st.Stacks[0].Parameters {
  617. parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue)
  618. }
  619. return parameters, nil
  620. }
  621. type stackResource struct {
  622. LogicalID string
  623. Type string
  624. ARN string
  625. Status string
  626. }
  627. type stackResourceFn func(r stackResource) error
  628. type stackResources []stackResource
  629. func (resources stackResources) apply(awsType string, fn stackResourceFn) error {
  630. var errs *multierror.Error
  631. for _, r := range resources {
  632. if r.Type == awsType {
  633. err := fn(r)
  634. if err != nil {
  635. errs = multierror.Append(err)
  636. }
  637. }
  638. }
  639. return errs.ErrorOrNil()
  640. }
  641. func (s sdk) ListStackResources(ctx context.Context, name string) (stackResources, error) {
  642. var token *string
  643. var resources stackResources
  644. for {
  645. response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  646. StackName: aws.String(name),
  647. })
  648. if err != nil {
  649. return nil, err
  650. }
  651. for _, r := range response.StackResourceSummaries {
  652. resources = append(resources, stackResource{
  653. LogicalID: aws.StringValue(r.LogicalResourceId),
  654. Type: aws.StringValue(r.ResourceType),
  655. ARN: aws.StringValue(r.PhysicalResourceId),
  656. Status: aws.StringValue(r.ResourceStatus),
  657. })
  658. }
  659. if token == response.NextToken {
  660. return resources, nil
  661. }
  662. token = response.NextToken
  663. }
  664. }
  665. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  666. logrus.Debug("Delete CloudFormation stack")
  667. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  668. StackName: aws.String(name),
  669. })
  670. return err
  671. }
  672. func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) {
  673. logrus.Debug("Create secret " + secret.Name)
  674. var tags []*secretsmanager.Tag
  675. for k, v := range secret.Labels {
  676. tags = []*secretsmanager.Tag{
  677. {
  678. Key: aws.String(k),
  679. Value: aws.String(v),
  680. },
  681. }
  682. }
  683. // store the secret content as string
  684. content := string(secret.GetContent())
  685. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  686. Name: &secret.Name,
  687. SecretString: &content,
  688. Tags: tags,
  689. })
  690. if err != nil {
  691. return "", err
  692. }
  693. return aws.StringValue(response.ARN), nil
  694. }
  695. func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) {
  696. logrus.Debug("Inspect secret " + id)
  697. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  698. if err != nil {
  699. return secrets.Secret{}, err
  700. }
  701. tags := map[string]string{}
  702. for _, tag := range response.Tags {
  703. tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
  704. }
  705. secret := secrets.Secret{
  706. ID: aws.StringValue(response.ARN),
  707. Name: aws.StringValue(response.Name),
  708. Labels: tags,
  709. }
  710. return secret, nil
  711. }
  712. func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) {
  713. logrus.Debug("List secrets ...")
  714. var ls []secrets.Secret
  715. var token *string
  716. for {
  717. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  718. if err != nil {
  719. return nil, err
  720. }
  721. for _, sec := range response.SecretList {
  722. tags := map[string]string{}
  723. for _, tag := range sec.Tags {
  724. tags[*tag.Key] = *tag.Value
  725. }
  726. ls = append(ls, secrets.Secret{
  727. ID: *sec.ARN,
  728. Name: *sec.Name,
  729. Labels: tags,
  730. })
  731. }
  732. if token == response.NextToken {
  733. return ls, nil
  734. }
  735. token = response.NextToken
  736. }
  737. }
  738. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  739. logrus.Debug("List secrets ...")
  740. force := !recover
  741. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  742. return err
  743. }
  744. func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error {
  745. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  746. var startTime int64
  747. for {
  748. select {
  749. case <-ctx.Done():
  750. return nil
  751. default:
  752. var hasMore = true
  753. var token *string
  754. for hasMore {
  755. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  756. LogGroupName: aws.String(logGroup),
  757. NextToken: token,
  758. StartTime: aws.Int64(startTime),
  759. })
  760. if err != nil {
  761. return err
  762. }
  763. if events.NextToken == nil {
  764. hasMore = false
  765. } else {
  766. token = events.NextToken
  767. }
  768. for _, event := range events.Events {
  769. p := strings.Split(aws.StringValue(event.LogStreamName), "/")
  770. consumer(p[1], p[2], aws.StringValue(event.Message))
  771. startTime = *event.IngestionTime
  772. }
  773. }
  774. }
  775. time.Sleep(500 * time.Millisecond)
  776. }
  777. }
  778. func (s sdk) DescribeService(ctx context.Context, cluster string, arn string) (compose.ServiceStatus, error) {
  779. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  780. Cluster: aws.String(cluster),
  781. Services: []*string{aws.String(arn)},
  782. Include: aws.StringSlice([]string{"TAGS"}),
  783. })
  784. if err != nil {
  785. return compose.ServiceStatus{}, err
  786. }
  787. for _, f := range services.Failures {
  788. return compose.ServiceStatus{}, errors.Wrapf(errdefs.ErrNotFound, "can't get service status %s: %s", aws.StringValue(f.Detail), aws.StringValue(f.Reason))
  789. }
  790. service := services.Services[0]
  791. var name string
  792. for _, t := range service.Tags {
  793. if *t.Key == compose.ServiceTag {
  794. name = aws.StringValue(t.Value)
  795. }
  796. }
  797. if name == "" {
  798. return compose.ServiceStatus{}, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  799. }
  800. targetGroupArns := []string{}
  801. for _, lb := range service.LoadBalancers {
  802. targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
  803. }
  804. // getURLwithPortMapping makes 2 queries
  805. // one to get the target groups and another for load balancers
  806. loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
  807. if err != nil {
  808. return compose.ServiceStatus{}, err
  809. }
  810. return compose.ServiceStatus{
  811. ID: aws.StringValue(service.ServiceName),
  812. Name: name,
  813. Replicas: int(aws.Int64Value(service.RunningCount)),
  814. Desired: int(aws.Int64Value(service.DesiredCount)),
  815. Publishers: loadBalancers,
  816. }, nil
  817. }
  818. func (s sdk) DescribeServiceTasks(ctx context.Context, cluster string, project string, service string) ([]compose.ContainerSummary, error) {
  819. var summary []compose.ContainerSummary
  820. familly := fmt.Sprintf("%s-%s", project, service)
  821. var token *string
  822. for {
  823. list, err := s.ECS.ListTasks(&ecs.ListTasksInput{
  824. Cluster: aws.String(cluster),
  825. Family: aws.String(familly),
  826. LaunchType: nil,
  827. MaxResults: nil,
  828. NextToken: token,
  829. })
  830. if err != nil {
  831. return nil, err
  832. }
  833. if len(list.TaskArns) == 0 {
  834. break
  835. }
  836. tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
  837. Cluster: aws.String(cluster),
  838. Include: aws.StringSlice([]string{"TAGS"}),
  839. Tasks: list.TaskArns,
  840. })
  841. if err != nil {
  842. return nil, err
  843. }
  844. for _, t := range tasks.Tasks {
  845. var project string
  846. var service string
  847. for _, tag := range t.Tags {
  848. switch aws.StringValue(tag.Key) {
  849. case compose.ProjectTag:
  850. project = aws.StringValue(tag.Value)
  851. case compose.ServiceTag:
  852. service = aws.StringValue(tag.Value)
  853. }
  854. }
  855. id, err := arn.Parse(aws.StringValue(t.TaskArn))
  856. if err != nil {
  857. return nil, err
  858. }
  859. summary = append(summary, compose.ContainerSummary{
  860. ID: id.String(),
  861. Name: id.Resource,
  862. Project: project,
  863. Service: service,
  864. State: strings.Title(strings.ToLower(aws.StringValue(t.LastStatus))),
  865. })
  866. }
  867. if list.NextToken == token {
  868. break
  869. }
  870. token = list.NextToken
  871. }
  872. return summary, nil
  873. }
  874. func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
  875. if len(targetGroupArns) == 0 {
  876. return nil, nil
  877. }
  878. groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
  879. TargetGroupArns: aws.StringSlice(targetGroupArns),
  880. })
  881. if err != nil {
  882. return nil, err
  883. }
  884. lbarns := []*string{}
  885. for _, tg := range groups.TargetGroups {
  886. lbarns = append(lbarns, tg.LoadBalancerArns...)
  887. }
  888. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  889. LoadBalancerArns: lbarns,
  890. })
  891. if err != nil {
  892. return nil, err
  893. }
  894. filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer {
  895. if aws.StringValue(arn) == "" {
  896. // load balancer arn is nil/""
  897. return nil
  898. }
  899. for _, lb := range lbs {
  900. if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) {
  901. return lb
  902. }
  903. }
  904. return nil
  905. }
  906. loadBalancers := []compose.PortPublisher{}
  907. for _, tg := range groups.TargetGroups {
  908. for _, lbarn := range tg.LoadBalancerArns {
  909. lb := filterLB(lbarn, lbs.LoadBalancers)
  910. if lb == nil {
  911. continue
  912. }
  913. loadBalancers = append(loadBalancers, compose.PortPublisher{
  914. URL: fmt.Sprintf("%s:%d", aws.StringValue(lb.DNSName), aws.Int64Value(tg.Port)),
  915. TargetPort: int(aws.Int64Value(tg.Port)),
  916. PublishedPort: int(aws.Int64Value(tg.Port)),
  917. Protocol: strings.ToLower(aws.StringValue(tg.Protocol)),
  918. })
  919. }
  920. }
  921. return loadBalancers, nil
  922. }
  923. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  924. var token *string
  925. var arns []string
  926. for {
  927. response, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  928. Cluster: aws.String(cluster),
  929. Family: aws.String(family),
  930. })
  931. if err != nil {
  932. return nil, err
  933. }
  934. for _, arn := range response.TaskArns {
  935. arns = append(arns, *arn)
  936. }
  937. if token == response.NextToken {
  938. return arns, nil
  939. }
  940. token = response.NextToken
  941. }
  942. }
  943. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  944. var token *string
  945. publicIPs := map[string]string{}
  946. for {
  947. response, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  948. NetworkInterfaceIds: aws.StringSlice(interfaces),
  949. })
  950. if err != nil {
  951. return nil, err
  952. }
  953. for _, interf := range response.NetworkInterfaces {
  954. if interf.Association != nil {
  955. publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp)
  956. }
  957. }
  958. if token == response.NextToken {
  959. return publicIPs, nil
  960. }
  961. token = response.NextToken
  962. }
  963. }
  964. func (s sdk) ResolveLoadBalancer(ctx context.Context, nameOrarn string) (awsResource, string, error) {
  965. logrus.Debug("Check if LoadBalancer exists: ", nameOrarn)
  966. var arns []*string
  967. var names []*string
  968. if arn.IsARN(nameOrarn) {
  969. arns = append(arns, aws.String(nameOrarn))
  970. } else {
  971. names = append(names, aws.String(nameOrarn))
  972. }
  973. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  974. LoadBalancerArns: arns,
  975. Names: names,
  976. })
  977. if err != nil {
  978. return nil, "", err
  979. }
  980. if len(lbs.LoadBalancers) == 0 {
  981. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "load balancer %q does not exist", nameOrarn)
  982. }
  983. it := lbs.LoadBalancers[0]
  984. return existingAWSResource{
  985. arn: aws.StringValue(it.LoadBalancerArn),
  986. id: aws.StringValue(it.LoadBalancerName),
  987. }, aws.StringValue(it.Type), nil
  988. }
  989. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  990. logrus.Debug("Retrieve load balancer URL: ", arn)
  991. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  992. LoadBalancerArns: []*string{aws.String(arn)},
  993. })
  994. if err != nil {
  995. return "", err
  996. }
  997. dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName)
  998. if dnsName == "" {
  999. return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn))
  1000. }
  1001. return dnsName, nil
  1002. }
  1003. func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
  1004. parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  1005. Name: aws.String(name),
  1006. })
  1007. if err != nil {
  1008. return "", err
  1009. }
  1010. value := *parameter.Parameter.Value
  1011. var ami struct {
  1012. SchemaVersion int `json:"schema_version"`
  1013. ImageName string `json:"image_name"`
  1014. ImageID string `json:"image_id"`
  1015. OS string `json:"os"`
  1016. ECSRuntimeVersion string `json:"ecs_runtime_verion"`
  1017. ECSAgentVersion string `json:"ecs_agent_version"`
  1018. }
  1019. err = json.Unmarshal([]byte(value), &ami)
  1020. if err != nil {
  1021. return "", err
  1022. }
  1023. return ami.ImageID, nil
  1024. }
  1025. func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
  1026. desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
  1027. GroupIds: aws.StringSlice([]string{sg}),
  1028. })
  1029. if err != nil {
  1030. return false, err
  1031. }
  1032. return len(desc.SecurityGroups) > 0, nil
  1033. }
  1034. func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
  1035. _, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
  1036. CapacityProvider: aws.String(arn),
  1037. })
  1038. return err
  1039. }
  1040. func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
  1041. _, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
  1042. AutoScalingGroupName: aws.String(arn),
  1043. ForceDelete: aws.Bool(true),
  1044. })
  1045. return err
  1046. }
  1047. func (s sdk) ResolveFileSystem(ctx context.Context, id string) (awsResource, error) {
  1048. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  1049. FileSystemId: aws.String(id),
  1050. })
  1051. if err != nil {
  1052. return nil, err
  1053. }
  1054. if len(desc.FileSystems) == 0 {
  1055. return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", id)
  1056. }
  1057. it := desc.FileSystems[0]
  1058. return existingAWSResource{
  1059. arn: aws.StringValue(it.FileSystemArn),
  1060. id: aws.StringValue(it.FileSystemId),
  1061. }, nil
  1062. }
  1063. func (s sdk) ListFileSystems(ctx context.Context, tags map[string]string) ([]awsResource, error) {
  1064. var results []awsResource
  1065. var token *string
  1066. for {
  1067. desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
  1068. Marker: token,
  1069. })
  1070. if err != nil {
  1071. return nil, err
  1072. }
  1073. for _, filesystem := range desc.FileSystems {
  1074. if containsAll(filesystem.Tags, tags) {
  1075. results = append(results, existingAWSResource{
  1076. arn: aws.StringValue(filesystem.FileSystemArn),
  1077. id: aws.StringValue(filesystem.FileSystemId),
  1078. })
  1079. }
  1080. }
  1081. if desc.NextMarker == token {
  1082. return results, nil
  1083. }
  1084. token = desc.NextMarker
  1085. }
  1086. }
  1087. func containsAll(tags []*efs.Tag, required map[string]string) bool {
  1088. TAGS:
  1089. for key, value := range required {
  1090. for _, t := range tags {
  1091. if aws.StringValue(t.Key) == key && aws.StringValue(t.Value) == value {
  1092. continue TAGS
  1093. }
  1094. }
  1095. return false
  1096. }
  1097. return true
  1098. }
  1099. func (s sdk) CreateFileSystem(ctx context.Context, tags map[string]string, options VolumeCreateOptions) (awsResource, error) {
  1100. var efsTags []*efs.Tag
  1101. for k, v := range tags {
  1102. efsTags = append(efsTags, &efs.Tag{
  1103. Key: aws.String(k),
  1104. Value: aws.String(v),
  1105. })
  1106. }
  1107. var (
  1108. k *string
  1109. p *string
  1110. f *float64
  1111. t *string
  1112. )
  1113. if options.ProvisionedThroughputInMibps > 1 {
  1114. f = aws.Float64(options.ProvisionedThroughputInMibps)
  1115. }
  1116. if options.KmsKeyID != "" {
  1117. k = aws.String(options.KmsKeyID)
  1118. }
  1119. if options.PerformanceMode != "" {
  1120. p = aws.String(options.PerformanceMode)
  1121. }
  1122. if options.ThroughputMode != "" {
  1123. t = aws.String(options.ThroughputMode)
  1124. }
  1125. res, err := s.EFS.CreateFileSystemWithContext(ctx, &efs.CreateFileSystemInput{
  1126. Encrypted: aws.Bool(true),
  1127. KmsKeyId: k,
  1128. PerformanceMode: p,
  1129. ProvisionedThroughputInMibps: f,
  1130. ThroughputMode: t,
  1131. Tags: efsTags,
  1132. })
  1133. if err != nil {
  1134. return nil, err
  1135. }
  1136. return existingAWSResource{
  1137. id: aws.StringValue(res.FileSystemId),
  1138. arn: aws.StringValue(res.FileSystemArn),
  1139. }, nil
  1140. }
  1141. func (s sdk) DeleteFileSystem(ctx context.Context, id string) error {
  1142. _, err := s.EFS.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{
  1143. FileSystemId: aws.String(id),
  1144. })
  1145. return err
  1146. }