cloudformation.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /*
  2. Copyright 2020 Docker, Inc.
  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. "io/ioutil"
  18. "regexp"
  19. "strings"
  20. "github.com/docker/compose-cli/compose"
  21. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  22. "github.com/aws/aws-sdk-go/service/elbv2"
  23. cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
  24. "github.com/awslabs/goformation/v4/cloudformation"
  25. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  26. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  27. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  28. "github.com/awslabs/goformation/v4/cloudformation/iam"
  29. "github.com/awslabs/goformation/v4/cloudformation/logs"
  30. "github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
  31. cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
  32. "github.com/awslabs/goformation/v4/cloudformation/tags"
  33. "github.com/compose-spec/compose-go/compatibility"
  34. "github.com/compose-spec/compose-go/errdefs"
  35. "github.com/compose-spec/compose-go/types"
  36. "github.com/sirupsen/logrus"
  37. )
  38. const (
  39. parameterClusterName = "ParameterClusterName"
  40. parameterVPCId = "ParameterVPCId"
  41. parameterSubnet1Id = "ParameterSubnet1Id"
  42. parameterSubnet2Id = "ParameterSubnet2Id"
  43. parameterLoadBalancerARN = "ParameterLoadBalancerARN"
  44. )
  45. func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
  46. template, err := b.convert(project)
  47. if err != nil {
  48. return nil, err
  49. }
  50. return marshall(template)
  51. }
  52. // Convert a compose project into a CloudFormation template
  53. func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo
  54. var checker compatibility.Checker = &fargateCompatibilityChecker{
  55. compatibility.AllowList{
  56. Supported: compatibleComposeAttributes,
  57. },
  58. }
  59. compatibility.Check(project, checker)
  60. for _, err := range checker.Errors() {
  61. if errdefs.IsIncompatibleError(err) {
  62. logrus.Error(err.Error())
  63. } else {
  64. logrus.Warn(err.Error())
  65. }
  66. }
  67. if !compatibility.IsCompatible(checker) {
  68. return nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
  69. }
  70. template := cloudformation.NewTemplate()
  71. template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS"
  72. template.Parameters[parameterClusterName] = cloudformation.Parameter{
  73. Type: "String",
  74. Description: "Name of the ECS cluster to deploy to (optional)",
  75. }
  76. template.Parameters[parameterVPCId] = cloudformation.Parameter{
  77. Type: "AWS::EC2::VPC::Id",
  78. Description: "ID of the VPC",
  79. }
  80. /*
  81. FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
  82. template.Parameters["SubnetIds"] = cloudformation.Parameter{
  83. Type: "List<AWS::EC2::Subnet::Id>",
  84. Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
  85. }
  86. */
  87. template.Parameters[parameterSubnet1Id] = cloudformation.Parameter{
  88. Type: "AWS::EC2::Subnet::Id",
  89. Description: "SubnetId, for Availability Zone 1 in the region in your VPC",
  90. }
  91. template.Parameters[parameterSubnet2Id] = cloudformation.Parameter{
  92. Type: "AWS::EC2::Subnet::Id",
  93. Description: "SubnetId, for Availability Zone 2 in the region in your VPC",
  94. }
  95. template.Parameters[parameterLoadBalancerARN] = cloudformation.Parameter{
  96. Type: "String",
  97. Description: "Name of the LoadBalancer to connect to (optional)",
  98. }
  99. // Create Cluster is `ParameterClusterName` parameter is not set
  100. template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName))
  101. cluster := createCluster(project, template)
  102. networks := map[string]string{}
  103. for _, net := range project.Networks {
  104. networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(parameterVPCId), template)
  105. }
  106. for i, s := range project.Secrets {
  107. if s.External.External {
  108. continue
  109. }
  110. secret, err := ioutil.ReadFile(s.File)
  111. if err != nil {
  112. return nil, err
  113. }
  114. name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
  115. template.Resources[name] = &secretsmanager.Secret{
  116. Description: "",
  117. SecretString: string(secret),
  118. Tags: []tags.Tag{
  119. {
  120. Key: compose.ProjectTag,
  121. Value: project.Name,
  122. },
  123. },
  124. }
  125. s.Name = cloudformation.Ref(name)
  126. project.Secrets[i] = s
  127. }
  128. createLogGroup(project, template)
  129. // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
  130. createCloudMap(project, template)
  131. loadBalancerARN := createLoadBalancer(project, template)
  132. for _, service := range project.Services {
  133. definition, err := convert(project, service)
  134. if err != nil {
  135. return nil, err
  136. }
  137. taskExecutionRole, err := createTaskExecutionRole(service, err, definition, template)
  138. if err != nil {
  139. return template, err
  140. }
  141. definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
  142. taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name))
  143. template.Resources[taskDefinition] = definition
  144. var healthCheck *cloudmap.Service_HealthCheckConfig
  145. serviceRegistry := createServiceRegistry(service, template, healthCheck)
  146. serviceSecurityGroups := []string{}
  147. for net := range service.Networks {
  148. serviceSecurityGroups = append(serviceSecurityGroups, networks[net])
  149. }
  150. dependsOn := []string{}
  151. serviceLB := []ecs.Service_LoadBalancer{}
  152. if len(service.Ports) > 0 {
  153. for _, port := range service.Ports {
  154. protocol := strings.ToUpper(port.Protocol)
  155. if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication {
  156. protocol = elbv2.ProtocolEnumHttps
  157. if port.Published == 80 {
  158. protocol = elbv2.ProtocolEnumHttp
  159. }
  160. }
  161. if loadBalancerARN != "" {
  162. targetGroupName := createTargetGroup(project, service, port, template, protocol)
  163. listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol)
  164. dependsOn = append(dependsOn, listenerName)
  165. serviceLB = append(serviceLB, ecs.Service_LoadBalancer{
  166. ContainerName: service.Name,
  167. ContainerPort: int(port.Target),
  168. TargetGroupArn: cloudformation.Ref(targetGroupName),
  169. })
  170. }
  171. }
  172. }
  173. desiredCount := 1
  174. if service.Deploy != nil && service.Deploy.Replicas != nil {
  175. desiredCount = int(*service.Deploy.Replicas)
  176. }
  177. for dependency := range service.DependsOn {
  178. dependsOn = append(dependsOn, serviceResourceName(dependency))
  179. }
  180. minPercent, maxPercent, err := computeRollingUpdateLimits(service)
  181. if err != nil {
  182. return nil, err
  183. }
  184. template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
  185. AWSCloudFormationDependsOn: dependsOn,
  186. Cluster: cluster,
  187. DesiredCount: desiredCount,
  188. DeploymentController: &ecs.Service_DeploymentController{
  189. Type: ecsapi.DeploymentControllerTypeEcs,
  190. },
  191. DeploymentConfiguration: &ecs.Service_DeploymentConfiguration{
  192. MaximumPercent: maxPercent,
  193. MinimumHealthyPercent: minPercent,
  194. },
  195. LaunchType: ecsapi.LaunchTypeFargate,
  196. LoadBalancers: serviceLB,
  197. NetworkConfiguration: &ecs.Service_NetworkConfiguration{
  198. AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
  199. AssignPublicIp: ecsapi.AssignPublicIpEnabled,
  200. SecurityGroups: serviceSecurityGroups,
  201. Subnets: []string{
  202. cloudformation.Ref(parameterSubnet1Id),
  203. cloudformation.Ref(parameterSubnet2Id),
  204. },
  205. },
  206. },
  207. PropagateTags: ecsapi.PropagateTagsService,
  208. SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
  209. ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
  210. Tags: []tags.Tag{
  211. {
  212. Key: compose.ProjectTag,
  213. Value: project.Name,
  214. },
  215. {
  216. Key: compose.ServiceTag,
  217. Value: service.Name,
  218. },
  219. },
  220. TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
  221. }
  222. }
  223. return template, nil
  224. }
  225. func createLogGroup(project *types.Project, template *cloudformation.Template) {
  226. retention := 0
  227. if v, ok := project.Extensions[extensionRetention]; ok {
  228. retention = v.(int)
  229. }
  230. logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
  231. template.Resources["LogGroup"] = &logs.LogGroup{
  232. LogGroupName: logGroup,
  233. RetentionInDays: retention,
  234. }
  235. }
  236. func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) {
  237. maxPercent := 200
  238. minPercent := 100
  239. if service.Deploy == nil || service.Deploy.UpdateConfig == nil {
  240. return minPercent, maxPercent, nil
  241. }
  242. updateConfig := service.Deploy.UpdateConfig
  243. min, okMin := updateConfig.Extensions[extensionMinPercent]
  244. if okMin {
  245. minPercent = min.(int)
  246. }
  247. max, okMax := updateConfig.Extensions[extensionMaxPercent]
  248. if okMax {
  249. maxPercent = max.(int)
  250. }
  251. if okMin && okMax {
  252. return minPercent, maxPercent, nil
  253. }
  254. if updateConfig.Parallelism != nil {
  255. parallelism := int(*updateConfig.Parallelism)
  256. if service.Deploy.Replicas == nil {
  257. return minPercent, maxPercent,
  258. fmt.Errorf("rolling update configuration require deploy.replicas to be set")
  259. }
  260. replicas := int(*service.Deploy.Replicas)
  261. if replicas < parallelism {
  262. return minPercent, maxPercent,
  263. fmt.Errorf("deploy.replicas (%d) must be greater than deploy.update_config.parallelism (%d)", replicas, parallelism)
  264. }
  265. if !okMin {
  266. minPercent = (replicas - parallelism) * 100 / replicas
  267. }
  268. if !okMax {
  269. maxPercent = (replicas + parallelism) * 100 / replicas
  270. }
  271. }
  272. return minPercent, maxPercent, nil
  273. }
  274. func getLoadBalancerType(project *types.Project) string {
  275. for _, service := range project.Services {
  276. for _, port := range service.Ports {
  277. protocol := port.Protocol
  278. v, ok := port.Extensions[extensionProtocol]
  279. if ok {
  280. protocol = v.(string)
  281. }
  282. if protocol == "http" || protocol == "https" {
  283. continue
  284. }
  285. if port.Published != 80 && port.Published != 443 {
  286. return elbv2.LoadBalancerTypeEnumNetwork
  287. }
  288. }
  289. }
  290. return elbv2.LoadBalancerTypeEnumApplication
  291. }
  292. func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string {
  293. securityGroups := []string{}
  294. for _, network := range project.Networks {
  295. if !network.Internal {
  296. net := convertNetwork(project, network, cloudformation.Ref(parameterVPCId), template)
  297. securityGroups = append(securityGroups, net)
  298. }
  299. }
  300. return uniqueStrings(securityGroups)
  301. }
  302. func createLoadBalancer(project *types.Project, template *cloudformation.Template) string {
  303. ports := 0
  304. for _, service := range project.Services {
  305. ports += len(service.Ports)
  306. }
  307. if ports == 0 {
  308. // Project do not expose any port (batch jobs?)
  309. // So no need to create a PortPublisher
  310. return ""
  311. }
  312. // load balancer names are limited to 32 characters total
  313. loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)))
  314. // Create PortPublisher if `ParameterLoadBalancerName` is not set
  315. template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(parameterLoadBalancerARN))
  316. loadBalancerType := getLoadBalancerType(project)
  317. securityGroups := []string{}
  318. if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication {
  319. securityGroups = getLoadBalancerSecurityGroups(project, template)
  320. }
  321. template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{
  322. Name: loadBalancerName,
  323. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  324. SecurityGroups: securityGroups,
  325. Subnets: []string{
  326. cloudformation.Ref(parameterSubnet1Id),
  327. cloudformation.Ref(parameterSubnet2Id),
  328. },
  329. Tags: []tags.Tag{
  330. {
  331. Key: compose.ProjectTag,
  332. Value: project.Name,
  333. },
  334. },
  335. Type: loadBalancerType,
  336. AWSCloudFormationCondition: "CreateLoadBalancer",
  337. }
  338. return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(parameterLoadBalancerARN))
  339. }
  340. func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string {
  341. listenerName := fmt.Sprintf(
  342. "%s%s%dListener",
  343. normalizeResourceName(service.Name),
  344. strings.ToUpper(port.Protocol),
  345. port.Target,
  346. )
  347. //add listener to dependsOn
  348. //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer
  349. template.Resources[listenerName] = &elasticloadbalancingv2.Listener{
  350. DefaultActions: []elasticloadbalancingv2.Listener_Action{
  351. {
  352. ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{
  353. TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{
  354. {
  355. TargetGroupArn: cloudformation.Ref(targetGroupName),
  356. },
  357. },
  358. },
  359. Type: elbv2.ActionTypeEnumForward,
  360. },
  361. },
  362. LoadBalancerArn: loadBalancerARN,
  363. Protocol: protocol,
  364. Port: int(port.Target),
  365. }
  366. return listenerName
  367. }
  368. func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string {
  369. targetGroupName := fmt.Sprintf(
  370. "%s%s%dTargetGroup",
  371. normalizeResourceName(service.Name),
  372. strings.ToUpper(port.Protocol),
  373. port.Published,
  374. )
  375. template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
  376. Port: int(port.Target),
  377. Protocol: protocol,
  378. Tags: []tags.Tag{
  379. {
  380. Key: compose.ProjectTag,
  381. Value: project.Name,
  382. },
  383. },
  384. VpcId: cloudformation.Ref(parameterVPCId),
  385. TargetType: elbv2.TargetTypeEnumIp,
  386. }
  387. return targetGroupName
  388. }
  389. func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry {
  390. serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name))
  391. serviceRegistry := ecs.Service_ServiceRegistry{
  392. RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
  393. }
  394. template.Resources[serviceRegistration] = &cloudmap.Service{
  395. Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name),
  396. HealthCheckConfig: healthCheck,
  397. HealthCheckCustomConfig: &cloudmap.Service_HealthCheckCustomConfig{
  398. FailureThreshold: 1,
  399. },
  400. Name: service.Name,
  401. NamespaceId: cloudformation.Ref("CloudMap"),
  402. DnsConfig: &cloudmap.Service_DnsConfig{
  403. DnsRecords: []cloudmap.Service_DnsRecord{
  404. {
  405. TTL: 60,
  406. Type: cloudmapapi.RecordTypeA,
  407. },
  408. },
  409. RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue,
  410. },
  411. }
  412. return serviceRegistry
  413. }
  414. func createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) {
  415. taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
  416. policy := getPolicy(definition)
  417. if err != nil {
  418. return taskExecutionRole, err
  419. }
  420. rolePolicies := []iam.Role_Policy{}
  421. if policy != nil {
  422. rolePolicies = append(rolePolicies, iam.Role_Policy{
  423. PolicyDocument: policy,
  424. PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name),
  425. })
  426. }
  427. if roles, ok := service.Extensions[extensionRole]; ok {
  428. rolePolicies = append(rolePolicies, iam.Role_Policy{
  429. PolicyDocument: roles,
  430. })
  431. }
  432. managedPolicies := []string{
  433. ecsTaskExecutionPolicy,
  434. ecrReadOnlyPolicy,
  435. }
  436. if v, ok := service.Extensions[extensionManagedPolicies]; ok {
  437. for _, s := range v.([]interface{}) {
  438. managedPolicies = append(managedPolicies, s.(string))
  439. }
  440. }
  441. template.Resources[taskExecutionRole] = &iam.Role{
  442. AssumeRolePolicyDocument: assumeRolePolicyDocument,
  443. Policies: rolePolicies,
  444. ManagedPolicyArns: managedPolicies,
  445. }
  446. return taskExecutionRole, nil
  447. }
  448. func createCluster(project *types.Project, template *cloudformation.Template) string {
  449. template.Resources["Cluster"] = &ecs.Cluster{
  450. ClusterName: project.Name,
  451. Tags: []tags.Tag{
  452. {
  453. Key: compose.ProjectTag,
  454. Value: project.Name,
  455. },
  456. },
  457. AWSCloudFormationCondition: "CreateCluster",
  458. }
  459. cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName))
  460. return cluster
  461. }
  462. func createCloudMap(project *types.Project, template *cloudformation.Template) {
  463. template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
  464. Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
  465. Name: fmt.Sprintf("%s.local", project.Name),
  466. Vpc: cloudformation.Ref(parameterVPCId),
  467. }
  468. }
  469. func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string {
  470. if sg, ok := net.Extensions[extensionSecurityGroup]; ok {
  471. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg)
  472. return sg.(string)
  473. }
  474. var ingresses []ec2.SecurityGroup_Ingress
  475. if !net.Internal {
  476. for _, service := range project.Services {
  477. if _, ok := service.Networks[net.Name]; ok {
  478. for _, port := range service.Ports {
  479. ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
  480. CidrIp: "0.0.0.0/0",
  481. Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
  482. FromPort: int(port.Target),
  483. IpProtocol: strings.ToUpper(port.Protocol),
  484. ToPort: int(port.Target),
  485. })
  486. }
  487. }
  488. }
  489. }
  490. securityGroup := networkResourceName(project, net.Name)
  491. template.Resources[securityGroup] = &ec2.SecurityGroup{
  492. GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name),
  493. GroupName: securityGroup,
  494. SecurityGroupIngress: ingresses,
  495. VpcId: vpc,
  496. Tags: []tags.Tag{
  497. {
  498. Key: compose.ProjectTag,
  499. Value: project.Name,
  500. },
  501. {
  502. Key: compose.NetworkTag,
  503. Value: net.Name,
  504. },
  505. },
  506. }
  507. ingress := securityGroup + "Ingress"
  508. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  509. Description: fmt.Sprintf("Allow communication within network %s", net.Name),
  510. IpProtocol: "-1", // all protocols
  511. GroupId: cloudformation.Ref(securityGroup),
  512. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  513. }
  514. return cloudformation.Ref(securityGroup)
  515. }
  516. func networkResourceName(project *types.Project, network string) string {
  517. return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network))
  518. }
  519. func serviceResourceName(dependency string) string {
  520. return fmt.Sprintf("%sService", normalizeResourceName(dependency))
  521. }
  522. func normalizeResourceName(s string) string {
  523. return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
  524. }
  525. func getPolicy(taskDef *ecs.TaskDefinition) *PolicyDocument {
  526. arns := []string{}
  527. for _, container := range taskDef.ContainerDefinitions {
  528. if container.RepositoryCredentials != nil {
  529. arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
  530. }
  531. if len(container.Secrets) > 0 {
  532. for _, s := range container.Secrets {
  533. arns = append(arns, s.ValueFrom)
  534. }
  535. }
  536. }
  537. if len(arns) > 0 {
  538. return &PolicyDocument{
  539. Statement: []PolicyStatement{
  540. {
  541. Effect: "Allow",
  542. Action: []string{actionGetSecretValue, actionGetParameters, actionDecrypt},
  543. Resource: arns,
  544. }},
  545. }
  546. }
  547. return nil
  548. }
  549. func uniqueStrings(items []string) []string {
  550. keys := make(map[string]bool)
  551. unique := []string{}
  552. for _, item := range items {
  553. if _, val := keys[item]; !val {
  554. keys[item] = true
  555. unique = append(unique, item)
  556. }
  557. }
  558. return unique
  559. }