sdk.go 12 KB

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