awsResources.go 12 KB

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