awsResources.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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.SDK.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.SDK.CheckVPC(ctx, vpc)
  89. if err != nil {
  90. return "", nil, err
  91. }
  92. } else {
  93. defaultVPC, err := b.SDK.GetDefaultVPC(ctx)
  94. if err != nil {
  95. return "", nil, err
  96. }
  97. vpc = defaultVPC
  98. }
  99. subNets, err := b.SDK.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.SDK.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. var sg string
  127. if net.External.External {
  128. sg = net.Name
  129. }
  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.SDK.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. securityGroup := networkResourceName(name)
  168. template.Resources[securityGroup] = &ec2.SecurityGroup{
  169. GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
  170. GroupName: securityGroup,
  171. VpcId: r.vpc,
  172. Tags: networkTags(project, net),
  173. }
  174. ingress := securityGroup + "Ingress"
  175. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  176. Description: fmt.Sprintf("Allow communication within network %s", name),
  177. IpProtocol: allProtocols,
  178. GroupId: cloudformation.Ref(securityGroup),
  179. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  180. }
  181. r.securityGroups[name] = cloudformation.Ref(securityGroup)
  182. }
  183. }
  184. func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
  185. if r.loadBalancer != "" {
  186. return
  187. }
  188. if allServices(project.Services, func(it types.ServiceConfig) bool {
  189. return len(it.Ports) == 0
  190. }) {
  191. logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
  192. return
  193. }
  194. balancerType := getRequiredLoadBalancerType(project)
  195. template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
  196. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  197. SecurityGroups: r.getLoadBalancerSecurityGroups(project),
  198. Subnets: r.subnets,
  199. Tags: projectTags(project),
  200. Type: balancerType,
  201. }
  202. r.loadBalancer = cloudformation.Ref("LoadBalancer")
  203. r.loadBalancerType = balancerType
  204. }
  205. func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
  206. securityGroups := []string{}
  207. for name, network := range project.Networks {
  208. if !network.Internal {
  209. securityGroups = append(securityGroups, r.securityGroups[name])
  210. }
  211. }
  212. return securityGroups
  213. }
  214. func getRequiredLoadBalancerType(project *types.Project) string {
  215. loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
  216. if allServices(project.Services, func(it types.ServiceConfig) bool {
  217. return allPorts(it.Ports, portIsHTTP)
  218. }) {
  219. loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
  220. }
  221. return loadBalancerType
  222. }
  223. func portIsHTTP(it types.ServicePortConfig) bool {
  224. if v, ok := it.Extensions[extensionProtocol]; ok {
  225. protocol := v.(string)
  226. return protocol == "http" || protocol == "https"
  227. }
  228. return it.Target == 80 || it.Target == 443
  229. }
  230. // predicate[types.ServiceConfig]
  231. type servicePredicate func(it types.ServiceConfig) bool
  232. // all[types.ServiceConfig]
  233. func allServices(services types.Services, p servicePredicate) bool {
  234. for _, s := range services {
  235. if !p(s) {
  236. return false
  237. }
  238. }
  239. return true
  240. }
  241. // predicate[types.ServicePortConfig]
  242. type portPredicate func(it types.ServicePortConfig) bool
  243. // all[types.ServicePortConfig]
  244. func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
  245. for _, s := range ports {
  246. if !p(s) {
  247. return false
  248. }
  249. }
  250. return true
  251. }