sdk.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. package sdk
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/docker/ecs-plugin/internal"
  8. "github.com/aws/aws-sdk-go/aws/request"
  9. "github.com/aws/aws-sdk-go/aws"
  10. "github.com/aws/aws-sdk-go/aws/session"
  11. "github.com/aws/aws-sdk-go/service/cloudformation"
  12. "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
  13. "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
  14. "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
  15. "github.com/aws/aws-sdk-go/service/ec2"
  16. "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
  17. "github.com/aws/aws-sdk-go/service/ecs"
  18. "github.com/aws/aws-sdk-go/service/ecs/ecsiface"
  19. "github.com/aws/aws-sdk-go/service/elbv2"
  20. "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
  21. "github.com/aws/aws-sdk-go/service/iam"
  22. "github.com/aws/aws-sdk-go/service/iam/iamiface"
  23. "github.com/aws/aws-sdk-go/service/secretsmanager"
  24. "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
  25. cf "github.com/awslabs/goformation/v4/cloudformation"
  26. "github.com/docker/ecs-plugin/pkg/compose"
  27. "github.com/sirupsen/logrus"
  28. )
  29. type sdk struct {
  30. sess *session.Session
  31. ECS ecsiface.ECSAPI
  32. EC2 ec2iface.EC2API
  33. ELB elbv2iface.ELBV2API
  34. CW cloudwatchlogsiface.CloudWatchLogsAPI
  35. IAM iamiface.IAMAPI
  36. CF cloudformationiface.CloudFormationAPI
  37. SM secretsmanageriface.SecretsManagerAPI
  38. }
  39. func NewAPI(sess *session.Session) API {
  40. sess.Handlers.Build.PushBack(func(r *request.Request) {
  41. request.AddToUserAgent(r, fmt.Sprintf("Docker CLI %s", internal.Version))
  42. })
  43. return sdk{
  44. ECS: ecs.New(sess),
  45. EC2: ec2.New(sess),
  46. ELB: elbv2.New(sess),
  47. CW: cloudwatchlogs.New(sess),
  48. IAM: iam.New(sess),
  49. CF: cloudformation.New(sess),
  50. SM: secretsmanager.New(sess),
  51. }
  52. }
  53. func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) {
  54. logrus.Debug("Check if cluster was already created: ", name)
  55. clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{
  56. Clusters: []*string{aws.String(name)},
  57. })
  58. if err != nil {
  59. return false, err
  60. }
  61. return len(clusters.Clusters) > 0, nil
  62. }
  63. func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) {
  64. logrus.Debug("Create cluster ", name)
  65. response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)})
  66. if err != nil {
  67. return "", err
  68. }
  69. return *response.Cluster.Status, nil
  70. }
  71. func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) {
  72. logrus.Debug("Check if VPC exists: ", vpcID)
  73. _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}})
  74. return err == nil, err
  75. }
  76. func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) {
  77. logrus.Debug("Retrieve default VPC")
  78. vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{
  79. Filters: []*ec2.Filter{
  80. {
  81. Name: aws.String("isDefault"),
  82. Values: []*string{aws.String("true")},
  83. },
  84. },
  85. })
  86. if err != nil {
  87. return "", err
  88. }
  89. if len(vpcs.Vpcs) == 0 {
  90. return "", fmt.Errorf("account has not default VPC")
  91. }
  92. return *vpcs.Vpcs[0].VpcId, nil
  93. }
  94. func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) {
  95. logrus.Debug("Retrieve SubNets")
  96. subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{
  97. DryRun: nil,
  98. Filters: []*ec2.Filter{
  99. {
  100. Name: aws.String("vpc-id"),
  101. Values: []*string{aws.String(vpcID)},
  102. },
  103. {
  104. Name: aws.String("default-for-az"),
  105. Values: []*string{aws.String("true")},
  106. },
  107. },
  108. })
  109. if err != nil {
  110. return nil, err
  111. }
  112. ids := []string{}
  113. for _, subnet := range subnets.Subnets {
  114. ids = append(ids, *subnet.SubnetId)
  115. }
  116. return ids, nil
  117. }
  118. func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) {
  119. role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{
  120. RoleName: aws.String(name),
  121. })
  122. if err != nil {
  123. return "", err
  124. }
  125. return *role.Role.Arn, nil
  126. }
  127. func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
  128. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  129. StackName: aws.String(name),
  130. })
  131. if err != nil {
  132. // FIXME doesn't work as expected
  133. return false, nil
  134. }
  135. return len(stacks.Stacks) > 0, nil
  136. }
  137. func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error {
  138. logrus.Debug("Create CloudFormation stack")
  139. json, err := template.JSON()
  140. if err != nil {
  141. return err
  142. }
  143. param := []*cloudformation.Parameter{}
  144. for name, value := range parameters {
  145. param = append(param, &cloudformation.Parameter{
  146. ParameterKey: aws.String(name),
  147. ParameterValue: aws.String(value),
  148. UsePreviousValue: aws.Bool(true),
  149. })
  150. }
  151. _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
  152. OnFailure: aws.String("DELETE"),
  153. StackName: aws.String(name),
  154. TemplateBody: aws.String(string(json)),
  155. Parameters: param,
  156. TimeoutInMinutes: aws.Int64(15),
  157. Capabilities: []*string{
  158. aws.String(cloudformation.CapabilityCapabilityIam),
  159. },
  160. })
  161. return err
  162. }
  163. func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error {
  164. input := &cloudformation.DescribeStacksInput{
  165. StackName: aws.String(name),
  166. }
  167. switch operation {
  168. case compose.StackCreate:
  169. return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input)
  170. case compose.StackDelete:
  171. return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input)
  172. default:
  173. return fmt.Errorf("internal error: unexpected stack operation %d", operation)
  174. }
  175. }
  176. func (s sdk) GetStackID(ctx context.Context, name string) (string, error) {
  177. stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{
  178. StackName: aws.String(name),
  179. })
  180. if err != nil {
  181. return "", err
  182. }
  183. return *stacks.Stacks[0].StackId, nil
  184. }
  185. func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
  186. // Fixme implement Paginator on Events and return as a chan(events)
  187. events := []*cloudformation.StackEvent{}
  188. var nextToken *string
  189. for {
  190. resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{
  191. StackName: aws.String(stackID),
  192. NextToken: nextToken,
  193. })
  194. if err != nil {
  195. return nil, err
  196. }
  197. events = append(events, resp.StackEvents...)
  198. if resp.NextToken == nil {
  199. return events, nil
  200. }
  201. nextToken = resp.NextToken
  202. }
  203. }
  204. func (s sdk) ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) {
  205. // FIXME handle pagination
  206. res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
  207. StackName: aws.String(name),
  208. })
  209. if err != nil {
  210. return nil, err
  211. }
  212. resources := []compose.StackResource{}
  213. for _, r := range res.StackResourceSummaries {
  214. resources = append(resources, compose.StackResource{
  215. LogicalID: *r.LogicalResourceId,
  216. Type: *r.ResourceType,
  217. ARN: *r.PhysicalResourceId,
  218. Status: *r.ResourceStatus,
  219. })
  220. }
  221. return resources, nil
  222. }
  223. func (s sdk) DeleteStack(ctx context.Context, name string) error {
  224. logrus.Debug("Delete CloudFormation stack")
  225. _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{
  226. StackName: aws.String(name),
  227. })
  228. return err
  229. }
  230. func (s sdk) CreateSecret(ctx context.Context, secret compose.Secret) (string, error) {
  231. logrus.Debug("Create secret " + secret.Name)
  232. secretStr, err := secret.GetCredString()
  233. if err != nil {
  234. return "", err
  235. }
  236. response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{
  237. Name: &secret.Name,
  238. SecretString: &secretStr,
  239. Description: &secret.Description,
  240. })
  241. if err != nil {
  242. return "", err
  243. }
  244. return *response.ARN, nil
  245. }
  246. func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, error) {
  247. logrus.Debug("Inspect secret " + id)
  248. response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
  249. if err != nil {
  250. return compose.Secret{}, err
  251. }
  252. labels := map[string]string{}
  253. for _, tag := range response.Tags {
  254. labels[*tag.Key] = *tag.Value
  255. }
  256. secret := compose.Secret{
  257. ID: *response.ARN,
  258. Name: *response.Name,
  259. Labels: labels,
  260. }
  261. if response.Description != nil {
  262. secret.Description = *response.Description
  263. }
  264. return secret, nil
  265. }
  266. func (s sdk) ListSecrets(ctx context.Context) ([]compose.Secret, error) {
  267. logrus.Debug("List secrets ...")
  268. response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
  269. if err != nil {
  270. return []compose.Secret{}, err
  271. }
  272. var secrets []compose.Secret
  273. for _, sec := range response.SecretList {
  274. labels := map[string]string{}
  275. for _, tag := range sec.Tags {
  276. labels[*tag.Key] = *tag.Value
  277. }
  278. description := ""
  279. if sec.Description != nil {
  280. description = *sec.Description
  281. }
  282. secrets = append(secrets, compose.Secret{
  283. ID: *sec.ARN,
  284. Name: *sec.Name,
  285. Labels: labels,
  286. Description: description,
  287. })
  288. }
  289. return secrets, nil
  290. }
  291. func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error {
  292. logrus.Debug("List secrets ...")
  293. force := !recover
  294. _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force})
  295. return err
  296. }
  297. func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsumer) error {
  298. logGroup := fmt.Sprintf("/docker-compose/%s", name)
  299. var startTime int64
  300. for {
  301. var hasMore = true
  302. var token *string
  303. for hasMore {
  304. events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{
  305. LogGroupName: aws.String(logGroup),
  306. NextToken: token,
  307. StartTime: aws.Int64(startTime),
  308. })
  309. if err != nil {
  310. return err
  311. }
  312. if events.NextToken == nil {
  313. hasMore = false
  314. } else {
  315. token = events.NextToken
  316. }
  317. for _, event := range events.Events {
  318. p := strings.Split(*event.LogStreamName, "/")
  319. consumer.Log(p[1], p[2], *event.Message)
  320. startTime = *event.IngestionTime
  321. }
  322. }
  323. time.Sleep(500 * time.Millisecond)
  324. }
  325. }
  326. func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) {
  327. services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
  328. Cluster: aws.String(cluster),
  329. Services: aws.StringSlice(arns),
  330. Include: aws.StringSlice([]string{"TAGS"}),
  331. })
  332. if err != nil {
  333. return nil, err
  334. }
  335. status := []compose.ServiceStatus{}
  336. for _, service := range services.Services {
  337. var name string
  338. for _, t := range service.Tags {
  339. if *t.Key == compose.ServiceTag {
  340. name = *t.Value
  341. }
  342. }
  343. if name == "" {
  344. return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
  345. }
  346. status = append(status, compose.ServiceStatus{
  347. ID: *service.ServiceName,
  348. Name: name,
  349. Replicas: int(*service.RunningCount),
  350. Desired: int(*service.DesiredCount),
  351. })
  352. }
  353. return status, nil
  354. }
  355. func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) {
  356. tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
  357. Cluster: aws.String(cluster),
  358. Family: aws.String(family),
  359. })
  360. if err != nil {
  361. return nil, err
  362. }
  363. arns := []string{}
  364. for _, arn := range tasks.TaskArns {
  365. arns = append(arns, *arn)
  366. }
  367. return arns, nil
  368. }
  369. func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) {
  370. desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
  371. NetworkInterfaceIds: aws.StringSlice(interfaces),
  372. })
  373. if err != nil {
  374. return nil, err
  375. }
  376. publicIPs := map[string]string{}
  377. for _, interf := range desc.NetworkInterfaces {
  378. if interf.Association != nil {
  379. publicIPs[*interf.NetworkInterfaceId] = *interf.Association.PublicIp
  380. }
  381. }
  382. return publicIPs, nil
  383. }
  384. func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) {
  385. logrus.Debug("Check if LoadBalancer exists: ", arn)
  386. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  387. LoadBalancerArns: []*string{aws.String(arn)},
  388. })
  389. if err != nil {
  390. return false, err
  391. }
  392. return len(lbs.LoadBalancers) > 0, nil
  393. }
  394. func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
  395. logrus.Debug("Retrieve load balancer URL: ", arn)
  396. lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
  397. LoadBalancerArns: []*string{aws.String(arn)},
  398. })
  399. if err != nil {
  400. return "", err
  401. }
  402. return *lbs.LoadBalancers[0].DNSName, nil
  403. }