cloudformation.go 19 KB

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