cloudformation.go 20 KB

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