awsResources.go 10 KB

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