awsResources.go 9.7 KB

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