awsResources.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. "github.com/aws/aws-sdk-go/service/elbv2"
  18. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  19. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  20. "github.com/awslabs/goformation/v4/cloudformation"
  21. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  22. "github.com/compose-spec/compose-go/types"
  23. "github.com/sirupsen/logrus"
  24. )
  25. // awsResources hold the AWS component being used or created to support services definition
  26. type awsResources struct {
  27. vpc string
  28. subnets []string
  29. cluster string
  30. loadBalancer string
  31. loadBalancerType string
  32. securityGroups map[string]string
  33. }
  34. func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string {
  35. var groups []string
  36. for net := range service.Networks {
  37. groups = append(groups, r.securityGroups[net])
  38. }
  39. return groups
  40. }
  41. func (r *awsResources) allSecurityGroups() []string {
  42. var securityGroups []string
  43. for _, r := range r.securityGroups {
  44. securityGroups = append(securityGroups, r)
  45. }
  46. return securityGroups
  47. }
  48. // parse look into compose project for configured resource to use, and check they are valid
  49. func (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsResources, error) {
  50. r := awsResources{}
  51. var err error
  52. r.cluster, err = b.parseClusterExtension(ctx, project)
  53. if err != nil {
  54. return r, err
  55. }
  56. r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
  57. if err != nil {
  58. return r, err
  59. }
  60. r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
  61. if err != nil {
  62. return r, err
  63. }
  64. r.securityGroups, err = b.parseSecurityGroupExtension(ctx, project)
  65. if err != nil {
  66. return r, err
  67. }
  68. return r, nil
  69. }
  70. func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project) (string, error) {
  71. if x, ok := project.Extensions[extensionCluster]; ok {
  72. cluster := x.(string)
  73. ok, err := b.aws.ClusterExists(ctx, cluster)
  74. if err != nil {
  75. return "", err
  76. }
  77. if !ok {
  78. return "", fmt.Errorf("cluster does not exist: %s", cluster)
  79. }
  80. return cluster, nil
  81. }
  82. return "", nil
  83. }
  84. func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []string, error) {
  85. var vpc string
  86. if x, ok := project.Extensions[extensionVPC]; ok {
  87. vpc = x.(string)
  88. err := b.aws.CheckVPC(ctx, vpc)
  89. if err != nil {
  90. return "", nil, err
  91. }
  92. } else {
  93. defaultVPC, err := b.aws.GetDefaultVPC(ctx)
  94. if err != nil {
  95. return "", nil, err
  96. }
  97. vpc = defaultVPC
  98. }
  99. subNets, err := b.aws.GetSubNets(ctx, vpc)
  100. if err != nil {
  101. return "", nil, err
  102. }
  103. if len(subNets) < 2 {
  104. return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
  105. }
  106. return vpc, subNets, nil
  107. }
  108. func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (string, string, error) {
  109. if x, ok := project.Extensions[extensionLoadBalancer]; ok {
  110. loadBalancer := x.(string)
  111. loadBalancerType, err := b.aws.LoadBalancerType(ctx, loadBalancer)
  112. if err != nil {
  113. return "", "", err
  114. }
  115. required := getRequiredLoadBalancerType(project)
  116. if loadBalancerType != required {
  117. return "", "", fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required)
  118. }
  119. return loadBalancer, loadBalancerType, nil
  120. }
  121. return "", "", nil
  122. }
  123. func (b *ecsAPIService) parseSecurityGroupExtension(ctx context.Context, project *types.Project) (map[string]string, error) {
  124. securityGroups := make(map[string]string, len(project.Networks))
  125. for name, net := range project.Networks {
  126. if !net.External.External {
  127. continue
  128. }
  129. sg := net.Name
  130. if x, ok := net.Extensions[extensionSecurityGroup]; ok {
  131. logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file")
  132. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
  133. sg = x.(string)
  134. }
  135. exists, err := b.aws.SecurityGroupExists(ctx, sg)
  136. if err != nil {
  137. return nil, err
  138. }
  139. if !exists {
  140. return nil, fmt.Errorf("security group %s doesn't exist", sg)
  141. }
  142. securityGroups[name] = sg
  143. }
  144. return securityGroups, nil
  145. }
  146. // ensureResources create required resources in template if not yet defined
  147. func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) {
  148. b.ensureCluster(resources, project, template)
  149. b.ensureNetworks(resources, project, template)
  150. b.ensureLoadBalancer(resources, project, template)
  151. }
  152. func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
  153. if r.cluster != "" {
  154. return
  155. }
  156. template.Resources["Cluster"] = &ecs.Cluster{
  157. ClusterName: project.Name,
  158. Tags: projectTags(project),
  159. }
  160. r.cluster = cloudformation.Ref("Cluster")
  161. }
  162. func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
  163. if r.securityGroups == nil {
  164. r.securityGroups = make(map[string]string, len(project.Networks))
  165. }
  166. for name, net := range project.Networks {
  167. if net.External.External {
  168. r.securityGroups[name] = net.Name
  169. continue
  170. }
  171. securityGroup := networkResourceName(name)
  172. template.Resources[securityGroup] = &ec2.SecurityGroup{
  173. GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
  174. VpcId: r.vpc,
  175. Tags: networkTags(project, net),
  176. }
  177. ingress := securityGroup + "Ingress"
  178. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  179. Description: fmt.Sprintf("Allow communication within network %s", name),
  180. IpProtocol: allProtocols,
  181. GroupId: cloudformation.Ref(securityGroup),
  182. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  183. }
  184. r.securityGroups[name] = cloudformation.Ref(securityGroup)
  185. }
  186. }
  187. func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
  188. if r.loadBalancer != "" {
  189. return
  190. }
  191. if allServices(project.Services, func(it types.ServiceConfig) bool {
  192. return len(it.Ports) == 0
  193. }) {
  194. logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
  195. return
  196. }
  197. balancerType := getRequiredLoadBalancerType(project)
  198. var securityGroups []string
  199. if balancerType == elbv2.LoadBalancerTypeEnumApplication {
  200. // see https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html#target-security-groups
  201. // Network Load Balancers do not have associated security groups
  202. securityGroups = r.getLoadBalancerSecurityGroups(project)
  203. }
  204. template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
  205. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  206. SecurityGroups: securityGroups,
  207. Subnets: r.subnets,
  208. Tags: projectTags(project),
  209. Type: balancerType,
  210. }
  211. r.loadBalancer = cloudformation.Ref("LoadBalancer")
  212. r.loadBalancerType = balancerType
  213. }
  214. func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
  215. securityGroups := []string{}
  216. for name, network := range project.Networks {
  217. if !network.Internal {
  218. securityGroups = append(securityGroups, r.securityGroups[name])
  219. }
  220. }
  221. return securityGroups
  222. }
  223. func getRequiredLoadBalancerType(project *types.Project) string {
  224. loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
  225. if allServices(project.Services, func(it types.ServiceConfig) bool {
  226. return allPorts(it.Ports, portIsHTTP)
  227. }) {
  228. loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
  229. }
  230. return loadBalancerType
  231. }
  232. func portIsHTTP(it types.ServicePortConfig) bool {
  233. if v, ok := it.Extensions[extensionProtocol]; ok {
  234. protocol := v.(string)
  235. return protocol == "http" || protocol == "https"
  236. }
  237. return it.Target == 80 || it.Target == 443
  238. }
  239. // predicate[types.ServiceConfig]
  240. type servicePredicate func(it types.ServiceConfig) bool
  241. // all[types.ServiceConfig]
  242. func allServices(services types.Services, p servicePredicate) bool {
  243. for _, s := range services {
  244. if !p(s) {
  245. return false
  246. }
  247. }
  248. return true
  249. }
  250. // predicate[types.ServicePortConfig]
  251. type portPredicate func(it types.ServicePortConfig) bool
  252. // all[types.ServicePortConfig]
  253. func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
  254. for _, s := range ports {
  255. if !p(s) {
  256. return false
  257. }
  258. }
  259. return true
  260. }