awsResources.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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
  34. subnets []string
  35. cluster string
  36. loadBalancer string
  37. loadBalancerType string
  38. securityGroups map[string]string
  39. filesystems map[string]string
  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. // parse look into compose project for configured resource to use, and check they are valid
  56. func (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsResources, error) {
  57. r := awsResources{}
  58. var err error
  59. r.cluster, err = b.parseClusterExtension(ctx, project)
  60. if err != nil {
  61. return r, err
  62. }
  63. r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
  64. if err != nil {
  65. return r, err
  66. }
  67. r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
  68. if err != nil {
  69. return r, err
  70. }
  71. r.securityGroups, err = b.parseExternalNetworks(ctx, project)
  72. if err != nil {
  73. return r, err
  74. }
  75. r.filesystems, err = b.parseExternalVolumes(ctx, project)
  76. if err != nil {
  77. return r, err
  78. }
  79. return r, nil
  80. }
  81. func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project) (string, error) {
  82. if x, ok := project.Extensions[extensionCluster]; ok {
  83. cluster := x.(string)
  84. ok, err := b.aws.ClusterExists(ctx, cluster)
  85. if err != nil {
  86. return "", err
  87. }
  88. if !ok {
  89. return "", errors.Wrapf(errdefs.ErrNotFound, "cluster %q does not exist", cluster)
  90. }
  91. return cluster, nil
  92. }
  93. return "", nil
  94. }
  95. func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []string, error) {
  96. var vpc string
  97. if x, ok := project.Extensions[extensionVPC]; ok {
  98. vpc = x.(string)
  99. err := b.aws.CheckVPC(ctx, vpc)
  100. if err != nil {
  101. return "", nil, err
  102. }
  103. } else {
  104. defaultVPC, err := b.aws.GetDefaultVPC(ctx)
  105. if err != nil {
  106. return "", nil, err
  107. }
  108. vpc = defaultVPC
  109. }
  110. subNets, err := b.aws.GetSubNets(ctx, vpc)
  111. if err != nil {
  112. return "", nil, err
  113. }
  114. if len(subNets) < 2 {
  115. return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
  116. }
  117. return vpc, subNets, nil
  118. }
  119. func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (string, string, error) {
  120. if x, ok := project.Extensions[extensionLoadBalancer]; ok {
  121. loadBalancer := x.(string)
  122. loadBalancerType, err := b.aws.LoadBalancerType(ctx, loadBalancer)
  123. if err != nil {
  124. return "", "", err
  125. }
  126. required := getRequiredLoadBalancerType(project)
  127. if loadBalancerType != required {
  128. return "", "", fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required)
  129. }
  130. return loadBalancer, loadBalancerType, nil
  131. }
  132. return "", "", nil
  133. }
  134. func (b *ecsAPIService) parseExternalNetworks(ctx context.Context, project *types.Project) (map[string]string, error) {
  135. securityGroups := make(map[string]string, len(project.Networks))
  136. for name, net := range project.Networks {
  137. // FIXME remove this for G.A
  138. if x, ok := net.Extensions[extensionSecurityGroup]; ok {
  139. logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file")
  140. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
  141. net.External.External = true
  142. net.Name = x.(string)
  143. project.Networks[name] = net
  144. }
  145. if !net.External.External {
  146. continue
  147. }
  148. exists, err := b.aws.SecurityGroupExists(ctx, net.Name)
  149. if err != nil {
  150. return nil, err
  151. }
  152. if !exists {
  153. return nil, errors.Wrapf(errdefs.ErrNotFound, "security group %q doesn't exist", net.Name)
  154. }
  155. securityGroups[name] = net.Name
  156. }
  157. return securityGroups, nil
  158. }
  159. func (b *ecsAPIService) parseExternalVolumes(ctx context.Context, project *types.Project) (map[string]string, error) {
  160. filesystems := make(map[string]string, len(project.Volumes))
  161. for name, vol := range project.Volumes {
  162. if vol.External.External {
  163. exists, err := b.aws.FileSystemExists(ctx, vol.Name)
  164. if err != nil {
  165. return nil, err
  166. }
  167. if !exists {
  168. return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", vol.Name)
  169. }
  170. filesystems[name] = vol.Name
  171. continue
  172. }
  173. logrus.Debugf("searching for existing filesystem as volume %q", name)
  174. tags := map[string]string{
  175. compose.ProjectTag: project.Name,
  176. compose.VolumeTag: name,
  177. }
  178. id, err := b.aws.FindFileSystem(ctx, tags)
  179. if err != nil {
  180. return nil, err
  181. }
  182. if id != "" {
  183. filesystems[name] = id
  184. }
  185. }
  186. return filesystems, nil
  187. }
  188. // ensureResources create required resources in template if not yet defined
  189. func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) error {
  190. b.ensureCluster(resources, project, template)
  191. b.ensureNetworks(resources, project, template)
  192. err := b.ensureVolumes(resources, project, template)
  193. if err != nil {
  194. return err
  195. }
  196. b.ensureLoadBalancer(resources, project, template)
  197. return nil
  198. }
  199. func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
  200. if r.cluster != "" {
  201. return
  202. }
  203. template.Resources["Cluster"] = &ecs.Cluster{
  204. ClusterName: project.Name,
  205. Tags: projectTags(project),
  206. }
  207. r.cluster = cloudformation.Ref("Cluster")
  208. }
  209. func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
  210. if r.securityGroups == nil {
  211. r.securityGroups = make(map[string]string, len(project.Networks))
  212. }
  213. for name, net := range project.Networks {
  214. if _, ok := r.securityGroups[name]; ok {
  215. continue
  216. }
  217. securityGroup := networkResourceName(name)
  218. template.Resources[securityGroup] = &ec2.SecurityGroup{
  219. GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
  220. VpcId: r.vpc,
  221. Tags: networkTags(project, net),
  222. }
  223. ingress := securityGroup + "Ingress"
  224. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  225. Description: fmt.Sprintf("Allow communication within network %s", name),
  226. IpProtocol: allProtocols,
  227. GroupId: cloudformation.Ref(securityGroup),
  228. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  229. }
  230. r.securityGroups[name] = cloudformation.Ref(securityGroup)
  231. }
  232. }
  233. func (b *ecsAPIService) ensureVolumes(r *awsResources, project *types.Project, template *cloudformation.Template) error {
  234. for name, volume := range project.Volumes {
  235. if _, ok := r.filesystems[name]; ok {
  236. continue
  237. }
  238. var backupPolicy *efs.FileSystem_BackupPolicy
  239. if backup, ok := volume.DriverOpts["backup_policy"]; ok {
  240. backupPolicy = &efs.FileSystem_BackupPolicy{
  241. Status: backup,
  242. }
  243. }
  244. var lifecyclePolicies []efs.FileSystem_LifecyclePolicy
  245. if policy, ok := volume.DriverOpts["lifecycle_policy"]; ok {
  246. lifecyclePolicies = append(lifecyclePolicies, efs.FileSystem_LifecyclePolicy{
  247. TransitionToIA: strings.TrimSpace(policy),
  248. })
  249. }
  250. var provisionedThroughputInMibps float64
  251. if t, ok := volume.DriverOpts["provisioned_throughput"]; ok {
  252. v, err := strconv.ParseFloat(t, 64)
  253. if err != nil {
  254. return err
  255. }
  256. provisionedThroughputInMibps = v
  257. }
  258. var performanceMode = volume.DriverOpts["performance_mode"]
  259. var throughputMode = volume.DriverOpts["throughput_mode"]
  260. var kmsKeyID = volume.DriverOpts["kms_key_id"]
  261. n := volumeResourceName(name)
  262. template.Resources[n] = &efs.FileSystem{
  263. BackupPolicy: backupPolicy,
  264. Encrypted: true,
  265. FileSystemPolicy: nil,
  266. FileSystemTags: []efs.FileSystem_ElasticFileSystemTag{
  267. {
  268. Key: compose.ProjectTag,
  269. Value: project.Name,
  270. },
  271. {
  272. Key: compose.VolumeTag,
  273. Value: name,
  274. },
  275. {
  276. Key: "Name",
  277. Value: fmt.Sprintf("%s_%s", project.Name, name),
  278. },
  279. },
  280. KmsKeyId: kmsKeyID,
  281. LifecyclePolicies: lifecyclePolicies,
  282. PerformanceMode: performanceMode,
  283. ProvisionedThroughputInMibps: provisionedThroughputInMibps,
  284. ThroughputMode: throughputMode,
  285. AWSCloudFormationDeletionPolicy: "Retain",
  286. }
  287. r.filesystems[name] = cloudformation.Ref(n)
  288. }
  289. return nil
  290. }
  291. func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
  292. if r.loadBalancer != "" {
  293. return
  294. }
  295. if allServices(project.Services, func(it types.ServiceConfig) bool {
  296. return len(it.Ports) == 0
  297. }) {
  298. logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
  299. return
  300. }
  301. balancerType := getRequiredLoadBalancerType(project)
  302. var securityGroups []string
  303. if balancerType == elbv2.LoadBalancerTypeEnumApplication {
  304. // see https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html#target-security-groups
  305. // Network Load Balancers do not have associated security groups
  306. securityGroups = r.getLoadBalancerSecurityGroups(project)
  307. }
  308. template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
  309. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  310. SecurityGroups: securityGroups,
  311. Subnets: r.subnets,
  312. Tags: projectTags(project),
  313. Type: balancerType,
  314. }
  315. r.loadBalancer = cloudformation.Ref("LoadBalancer")
  316. r.loadBalancerType = balancerType
  317. }
  318. func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
  319. securityGroups := []string{}
  320. for name, network := range project.Networks {
  321. if !network.Internal {
  322. securityGroups = append(securityGroups, r.securityGroups[name])
  323. }
  324. }
  325. return securityGroups
  326. }
  327. func getRequiredLoadBalancerType(project *types.Project) string {
  328. loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
  329. if allServices(project.Services, func(it types.ServiceConfig) bool {
  330. return allPorts(it.Ports, portIsHTTP)
  331. }) {
  332. loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
  333. }
  334. return loadBalancerType
  335. }
  336. func portIsHTTP(it types.ServicePortConfig) bool {
  337. if v, ok := it.Extensions[extensionProtocol]; ok {
  338. protocol := v.(string)
  339. return protocol == "http" || protocol == "https"
  340. }
  341. return it.Target == 80 || it.Target == 443
  342. }
  343. // predicate[types.ServiceConfig]
  344. type servicePredicate func(it types.ServiceConfig) bool
  345. // all[types.ServiceConfig]
  346. func allServices(services types.Services, p servicePredicate) bool {
  347. for _, s := range services {
  348. if !p(s) {
  349. return false
  350. }
  351. }
  352. return true
  353. }
  354. // predicate[types.ServicePortConfig]
  355. type portPredicate func(it types.ServicePortConfig) bool
  356. // all[types.ServicePortConfig]
  357. func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
  358. for _, s := range ports {
  359. if !p(s) {
  360. return false
  361. }
  362. }
  363. return true
  364. }