cloudformation.go 19 KB

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