sdk.go 29 KB

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