awsResources.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. "fmt"
  17. "strconv"
  18. "strings"
  19. "github.com/docker/compose-cli/api/compose"
  20. "github.com/docker/compose-cli/errdefs"
  21. "github.com/aws/aws-sdk-go/service/elbv2"
  22. "github.com/awslabs/goformation/v4/cloudformation"
  23. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  24. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  25. "github.com/awslabs/goformation/v4/cloudformation/efs"
  26. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  27. "github.com/compose-spec/compose-go/types"
  28. "github.com/pkg/errors"
  29. "github.com/sirupsen/logrus"
  30. )
  31. // awsResources hold the AWS component being used or created to support services definition
  32. type awsResources struct {
  33. vpc string // shouldn't this also be an awsResource ?
  34. subnets []awsResource
  35. cluster awsResource
  36. loadBalancer awsResource
  37. loadBalancerType string
  38. securityGroups map[string]string
  39. filesystems map[string]awsResource
  40. }
  41. func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string {
  42. var groups []string
  43. for net := range service.Networks {
  44. groups = append(groups, r.securityGroups[net])
  45. }
  46. return groups
  47. }
  48. func (r *awsResources) allSecurityGroups() []string {
  49. var securityGroups []string
  50. for _, r := range r.securityGroups {
  51. securityGroups = append(securityGroups, r)
  52. }
  53. return securityGroups
  54. }
  55. func (r *awsResources) subnetsIDs() []string {
  56. var ids []string
  57. for _, r := range r.subnets {
  58. ids = append(ids, r.ID())
  59. }
  60. return ids
  61. }
  62. // awsResource is abstract representation for any (existing or future) AWS resource that we can refer both by ID or full ARN
  63. type awsResource interface {
  64. ARN() string
  65. ID() string
  66. }
  67. // existingAWSResource hold references to an existing AWS component
  68. type existingAWSResource struct {
  69. arn string
  70. id string
  71. }
  72. func (r existingAWSResource) ARN() string {
  73. return r.arn
  74. }
  75. func (r existingAWSResource) ID() string {
  76. return r.id
  77. }
  78. // cloudformationResource hold references to a future AWS resource managed by CloudFormation
  79. // to be used by CloudFormation resources where Ref returns the Amazon Resource ID
  80. type cloudformationResource struct {
  81. logicalName string
  82. }
  83. func (r cloudformationResource) ARN() string {
  84. return cloudformation.GetAtt(r.logicalName, "Arn")
  85. }
  86. func (r cloudformationResource) ID() string {
  87. return cloudformation.Ref(r.logicalName)
  88. }
  89. // cloudformationARNResource hold references to a future AWS resource managed by CloudFormation
  90. // to be used by CloudFormation resources where Ref returns the Amazon Resource Name (ARN)
  91. type cloudformationARNResource struct {
  92. logicalName string
  93. nameProperty string
  94. }
  95. func (r cloudformationARNResource) ARN() string {
  96. return cloudformation.Ref(r.logicalName)
  97. }
  98. func (r cloudformationARNResource) ID() string {
  99. return cloudformation.GetAtt(r.logicalName, r.nameProperty)
  100. }
  101. // parse look into compose project for configured resource to use, and check they are valid
  102. func (b *ecsAPIService) parse(ctx context.Context, project *types.Project, template *cloudformation.Template) (awsResources, error) {
  103. r := awsResources{}
  104. var err error
  105. r.cluster, err = b.parseClusterExtension(ctx, project, template)
  106. if err != nil {
  107. return r, err
  108. }
  109. r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
  110. if err != nil {
  111. return r, err
  112. }
  113. r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
  114. if err != nil {
  115. return r, err
  116. }
  117. r.securityGroups, err = b.parseExternalNetworks(ctx, project)
  118. if err != nil {
  119. return r, err
  120. }
  121. r.filesystems, err = b.parseExternalVolumes(ctx, project)
  122. if err != nil {
  123. return r, err
  124. }
  125. return r, nil
  126. }
  127. func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project, template *cloudformation.Template) (awsResource, error) {
  128. if x, ok := project.Extensions[extensionCluster]; ok {
  129. nameOrArn := x.(string) // can be name _or_ ARN.
  130. cluster, err := b.aws.ResolveCluster(ctx, nameOrArn)
  131. if err != nil {
  132. return nil, err
  133. }
  134. if !ok {
  135. return nil, errors.Wrapf(errdefs.ErrNotFound, "cluster %q does not exist", cluster)
  136. }
  137. template.Metadata["Cluster"] = cluster.ARN()
  138. return cluster, nil
  139. }
  140. return nil, nil
  141. }
  142. func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []awsResource, error) {
  143. var vpc string
  144. if x, ok := project.Extensions[extensionVPC]; ok {
  145. vpcID := x.(string)
  146. err := b.aws.CheckVPC(ctx, vpcID)
  147. if err != nil {
  148. return "", nil, err
  149. }
  150. } else {
  151. defaultVPC, err := b.aws.GetDefaultVPC(ctx)
  152. if err != nil {
  153. return "", nil, err
  154. }
  155. vpc = defaultVPC
  156. }
  157. subNets, err := b.aws.GetSubNets(ctx, vpc)
  158. if err != nil {
  159. return "", nil, err
  160. }
  161. if len(subNets) < 2 {
  162. return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
  163. }
  164. return vpc, subNets, nil
  165. }
  166. func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (awsResource, string, error) {
  167. if x, ok := project.Extensions[extensionLoadBalancer]; ok {
  168. nameOrArn := x.(string)
  169. loadBalancer, loadBalancerType, err := b.aws.ResolveLoadBalancer(ctx, nameOrArn)
  170. if err != nil {
  171. return nil, "", err
  172. }
  173. required := getRequiredLoadBalancerType(project)
  174. if loadBalancerType != required {
  175. return nil, "", fmt.Errorf("load balancer %q is of type %s, project require a %s", nameOrArn, loadBalancerType, required)
  176. }
  177. return loadBalancer, loadBalancerType, err
  178. }
  179. return nil, "", nil
  180. }
  181. func (b *ecsAPIService) parseExternalNetworks(ctx context.Context, project *types.Project) (map[string]string, error) {
  182. securityGroups := make(map[string]string, len(project.Networks))
  183. for name, net := range project.Networks {
  184. // FIXME remove this for G.A
  185. if x, ok := net.Extensions[extensionSecurityGroup]; ok {
  186. logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file")
  187. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
  188. net.External.External = true
  189. net.Name = x.(string)
  190. project.Networks[name] = net
  191. }
  192. if !net.External.External {
  193. continue
  194. }
  195. exists, err := b.aws.SecurityGroupExists(ctx, net.Name)
  196. if err != nil {
  197. return nil, err
  198. }
  199. if !exists {
  200. return nil, errors.Wrapf(errdefs.ErrNotFound, "security group %q doesn't exist", net.Name)
  201. }
  202. securityGroups[name] = net.Name
  203. }
  204. return securityGroups, nil
  205. }
  206. func (b *ecsAPIService) parseExternalVolumes(ctx context.Context, project *types.Project) (map[string]awsResource, error) {
  207. filesystems := make(map[string]awsResource, len(project.Volumes))
  208. for name, vol := range project.Volumes {
  209. if vol.External.External {
  210. arn, err := b.aws.ResolveFileSystem(ctx, vol.Name)
  211. if err != nil {
  212. return nil, err
  213. }
  214. filesystems[name] = arn
  215. continue
  216. }
  217. logrus.Debugf("searching for existing filesystem as volume %q", name)
  218. tags := map[string]string{
  219. compose.ProjectTag: project.Name,
  220. compose.VolumeTag: name,
  221. }
  222. previous, err := b.aws.ListFileSystems(ctx, tags)
  223. if err != nil {
  224. return nil, err
  225. }
  226. if len(previous) > 1 {
  227. return nil, fmt.Errorf("multiple filesystems are tags as project=%q, volume=%q", project.Name, name)
  228. }
  229. if len(previous) == 1 {
  230. filesystems[name] = previous[0]
  231. }
  232. }
  233. return filesystems, nil
  234. }
  235. // ensureResources create required resources in template if not yet defined
  236. func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) error {
  237. b.ensureCluster(resources, project, template)
  238. b.ensureNetworks(resources, project, template)
  239. err := b.ensureVolumes(resources, project, template)
  240. if err != nil {
  241. return err
  242. }
  243. b.ensureLoadBalancer(resources, project, template)
  244. return nil
  245. }
  246. func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
  247. if r.cluster != nil {
  248. return
  249. }
  250. template.Resources["Cluster"] = &ecs.Cluster{
  251. ClusterName: project.Name,
  252. Tags: projectTags(project),
  253. }
  254. r.cluster = cloudformationResource{logicalName: "Cluster"}
  255. }
  256. func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
  257. if r.securityGroups == nil {
  258. r.securityGroups = make(map[string]string, len(project.Networks))
  259. }
  260. for name, net := range project.Networks {
  261. if _, ok := r.securityGroups[name]; ok {
  262. continue
  263. }
  264. securityGroup := networkResourceName(name)
  265. template.Resources[securityGroup] = &ec2.SecurityGroup{
  266. GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
  267. VpcId: r.vpc,
  268. Tags: networkTags(project, net),
  269. }
  270. ingress := securityGroup + "Ingress"
  271. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  272. Description: fmt.Sprintf("Allow communication within network %s", name),
  273. IpProtocol: allProtocols,
  274. GroupId: cloudformation.Ref(securityGroup),
  275. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  276. }
  277. r.securityGroups[name] = cloudformation.Ref(securityGroup)
  278. }
  279. }
  280. func (b *ecsAPIService) ensureVolumes(r *awsResources, project *types.Project, template *cloudformation.Template) error {
  281. for name, volume := range project.Volumes {
  282. if _, ok := r.filesystems[name]; ok {
  283. continue
  284. }
  285. var backupPolicy *efs.FileSystem_BackupPolicy
  286. if backup, ok := volume.DriverOpts["backup_policy"]; ok {
  287. backupPolicy = &efs.FileSystem_BackupPolicy{
  288. Status: backup,
  289. }
  290. }
  291. var lifecyclePolicies []efs.FileSystem_LifecyclePolicy
  292. if policy, ok := volume.DriverOpts["lifecycle_policy"]; ok {
  293. lifecyclePolicies = append(lifecyclePolicies, efs.FileSystem_LifecyclePolicy{
  294. TransitionToIA: strings.TrimSpace(policy),
  295. })
  296. }
  297. var provisionedThroughputInMibps float64
  298. if t, ok := volume.DriverOpts["provisioned_throughput"]; ok {
  299. v, err := strconv.ParseFloat(t, 64)
  300. if err != nil {
  301. return err
  302. }
  303. provisionedThroughputInMibps = v
  304. }
  305. var performanceMode = volume.DriverOpts["performance_mode"]
  306. var throughputMode = volume.DriverOpts["throughput_mode"]
  307. var kmsKeyID = volume.DriverOpts["kms_key_id"]
  308. n := volumeResourceName(name)
  309. template.Resources[n] = &efs.FileSystem{
  310. BackupPolicy: backupPolicy,
  311. Encrypted: true,
  312. FileSystemPolicy: nil,
  313. FileSystemTags: []efs.FileSystem_ElasticFileSystemTag{
  314. {
  315. Key: compose.ProjectTag,
  316. Value: project.Name,
  317. },
  318. {
  319. Key: compose.VolumeTag,
  320. Value: name,
  321. },
  322. {
  323. Key: "Name",
  324. Value: fmt.Sprintf("%s_%s", project.Name, name),
  325. },
  326. },
  327. KmsKeyId: kmsKeyID,
  328. LifecyclePolicies: lifecyclePolicies,
  329. PerformanceMode: performanceMode,
  330. ProvisionedThroughputInMibps: provisionedThroughputInMibps,
  331. ThroughputMode: throughputMode,
  332. AWSCloudFormationDeletionPolicy: "Retain",
  333. }
  334. r.filesystems[name] = cloudformationResource{logicalName: n}
  335. }
  336. return nil
  337. }
  338. func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
  339. if r.loadBalancer != nil {
  340. return
  341. }
  342. if allServices(project.Services, func(it types.ServiceConfig) bool {
  343. return len(it.Ports) == 0
  344. }) {
  345. logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
  346. return
  347. }
  348. balancerType := getRequiredLoadBalancerType(project)
  349. var securityGroups []string
  350. if balancerType == elbv2.LoadBalancerTypeEnumApplication {
  351. // see https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html#target-security-groups
  352. // Network Load Balancers do not have associated security groups
  353. securityGroups = r.getLoadBalancerSecurityGroups(project)
  354. }
  355. var loadBalancerAttributes []elasticloadbalancingv2.LoadBalancer_LoadBalancerAttribute
  356. if balancerType == elbv2.LoadBalancerTypeEnumNetwork {
  357. loadBalancerAttributes = append(
  358. loadBalancerAttributes,
  359. elasticloadbalancingv2.LoadBalancer_LoadBalancerAttribute{
  360. Key: "load_balancing.cross_zone.enabled",
  361. Value: "true",
  362. })
  363. }
  364. template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
  365. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  366. SecurityGroups: securityGroups,
  367. Subnets: r.subnetsIDs(),
  368. Tags: projectTags(project),
  369. Type: balancerType,
  370. LoadBalancerAttributes: loadBalancerAttributes,
  371. }
  372. r.loadBalancer = cloudformationARNResource{
  373. logicalName: "LoadBalancer",
  374. nameProperty: "LoadBalancerName",
  375. }
  376. r.loadBalancerType = balancerType
  377. }
  378. func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
  379. securityGroups := []string{}
  380. for name, network := range project.Networks {
  381. if !network.Internal {
  382. securityGroups = append(securityGroups, r.securityGroups[name])
  383. }
  384. }
  385. return securityGroups
  386. }
  387. func getRequiredLoadBalancerType(project *types.Project) string {
  388. loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
  389. if allServices(project.Services, func(it types.ServiceConfig) bool {
  390. return allPorts(it.Ports, portIsHTTP)
  391. }) {
  392. loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
  393. }
  394. return loadBalancerType
  395. }
  396. func portIsHTTP(it types.ServicePortConfig) bool {
  397. if v, ok := it.Extensions[extensionProtocol]; ok {
  398. protocol := v.(string)
  399. return protocol == "http" || protocol == "https"
  400. }
  401. return it.Target == 80 || it.Target == 443
  402. }
  403. // predicate[types.ServiceConfig]
  404. type servicePredicate func(it types.ServiceConfig) bool
  405. // all[types.ServiceConfig]
  406. func allServices(services types.Services, p servicePredicate) bool {
  407. for _, s := range services {
  408. if !p(s) {
  409. return false
  410. }
  411. }
  412. return true
  413. }
  414. // predicate[types.ServicePortConfig]
  415. type portPredicate func(it types.ServicePortConfig) bool
  416. // all[types.ServicePortConfig]
  417. func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
  418. for _, s := range ports {
  419. if !p(s) {
  420. return false
  421. }
  422. }
  423. return true
  424. }