github.com/GoogleCloudPlatform/terraformer@v0.8.18/providers/aws/sg.go (about) 1 // Copyright 2018 The Terraformer Authors. 2 // 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 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package aws 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "os" 22 "sort" 23 "strings" 24 25 "github.com/GoogleCloudPlatform/terraformer/terraformutils" 26 "github.com/aws/aws-sdk-go-v2/service/ec2" 27 "github.com/aws/aws-sdk-go-v2/service/ec2/types" 28 "github.com/hashicorp/terraform/flatmap" 29 "gonum.org/v1/gonum/graph" 30 simplegraph "gonum.org/v1/gonum/graph/simple" 31 "gonum.org/v1/gonum/graph/topo" 32 ) 33 34 var SgAllowEmptyValues = []string{"tags."} 35 36 type void struct{} 37 38 var member void 39 40 type SecurityGenerator struct { 41 AWSService 42 } 43 44 type ByGroupPair []types.UserIdGroupPair 45 46 func (b ByGroupPair) Len() int { return len(b) } 47 func (b ByGroupPair) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 48 func (b ByGroupPair) Less(i, j int) bool { 49 if b[i].GroupId != nil && b[j].GroupId != nil { 50 return *b[i].GroupId < *b[j].GroupId 51 } 52 if b[i].GroupName != nil && b[j].GroupName != nil { 53 return *b[i].GroupName < *b[j].GroupName 54 } 55 56 panic("mismatched security group rules, may be a terraform bug") 57 } 58 59 func (SecurityGenerator) createResources(securityGroups []types.SecurityGroup) []terraformutils.Resource { 60 var sgIDsToMoveOut []string 61 _, shouldSplitRules := os.LookupEnv("SPLIT_SG_RULES") 62 if shouldSplitRules { 63 for _, sg := range securityGroups { 64 sgIDsToMoveOut = append(sgIDsToMoveOut, *sg.GroupId) 65 } 66 } else { 67 sgIDsToMoveOut = findSgsToMoveOut(securityGroups) 68 } 69 70 var resources []terraformutils.Resource 71 for _, sg := range securityGroups { 72 if sg.VpcId == nil { 73 continue 74 } 75 ruleAttributes := map[string]interface{}{} 76 // we must move out all of the rules - https://github.com/hashicorp/terraform/issues/11011#issuecomment-283076580 77 for _, groupIDToMoveOut := range sgIDsToMoveOut { 78 if groupIDToMoveOut == *sg.GroupId { 79 ruleAttributes["clearRules"] = true 80 for _, rule := range sg.IpPermissions { 81 resources = processRule(rule, "ingress", sg, resources) 82 } 83 for _, rule := range sg.IpPermissionsEgress { 84 resources = processRule(rule, "egress", sg, resources) 85 } 86 } 87 } 88 89 resources = append(resources, terraformutils.NewResource( 90 StringValue(sg.GroupId), 91 strings.Trim(StringValue(sg.GroupName)+"_"+StringValue(sg.GroupId), " "), 92 "aws_security_group", 93 "aws", 94 map[string]string{}, 95 SgAllowEmptyValues, 96 ruleAttributes)) 97 } 98 return resources 99 } 100 101 func processRule(rule types.IpPermission, ruleType string, sg types.SecurityGroup, resources []terraformutils.Resource) []terraformutils.Resource { 102 if rule.UserIdGroupPairs != nil && len(rule.UserIdGroupPairs) > 0 { 103 if len(rule.IpRanges) > 0 { // we must unwind coupled CIDR IPv4 range + security group rules 104 attributes := baseRuleAttributes(ruleType, rule, sg) 105 resources = append(resources, terraformutils.NewResource( 106 permissionID(*sg.GroupId, ruleType, "", rule), 107 permissionID(*sg.GroupId, ruleType, "", rule), 108 "aws_security_group_rule", 109 "aws", 110 flatmap.Flatten(attributes), 111 SgAllowEmptyValues, 112 map[string]interface{}{})) 113 } 114 if len(rule.Ipv6Ranges) > 0 { // we must unwind coupled CIDR IPv6 range + security group rules 115 attributes := baseRuleAttributes(ruleType, rule, sg) 116 resources = append(resources, terraformutils.NewResource( 117 permissionID(*sg.GroupId, ruleType, "", rule), 118 permissionID(*sg.GroupId, ruleType, "", rule), 119 "aws_security_group_rule", 120 "aws", 121 flatmap.Flatten(attributes), 122 SgAllowEmptyValues, 123 map[string]interface{}{})) 124 } 125 for _, groupPair := range rule.UserIdGroupPairs { 126 attributes := baseRuleAttributes(ruleType, rule, sg) 127 delete(attributes, "cidr_blocks") 128 delete(attributes, "ipv6_cidr_blocks") 129 if *groupPair.GroupId == *sg.GroupId { // Solution to C1 130 attributes["self"] = true 131 } else { 132 attributes["source_security_group_id"] = *groupPair.GroupId 133 } 134 135 resources = append(resources, terraformutils.NewResource( 136 permissionID(*sg.GroupId, ruleType, *groupPair.GroupId, rule), 137 permissionID(*sg.GroupId, ruleType, *groupPair.GroupId, rule), 138 "aws_security_group_rule", 139 "aws", 140 flatmap.Flatten(attributes), 141 SgAllowEmptyValues, 142 map[string]interface{}{})) 143 } 144 } else { 145 attributes := baseRuleAttributes(ruleType, rule, sg) 146 resources = append(resources, terraformutils.NewResource( 147 permissionID(*sg.GroupId, ruleType, "", rule), 148 permissionID(*sg.GroupId, ruleType, "", rule), 149 "aws_security_group_rule", 150 "aws", 151 flatmap.Flatten(attributes), 152 SgAllowEmptyValues, 153 map[string]interface{}{})) 154 } 155 return resources 156 } 157 158 func baseRuleAttributes(ruleType string, rule types.IpPermission, sg types.SecurityGroup) map[string]interface{} { 159 attributes := map[string]interface{}{ 160 "type": ruleType, 161 "cidr_blocks": ipRange(rule), 162 "ipv6_cidr_blocks": ip6Range(rule), 163 "prefix_list_ids": prefixes(rule), 164 "from_port": fromPort(rule), 165 "protocol": *rule.IpProtocol, 166 "security_group_id": *sg.GroupId, 167 "to_port": toPort(rule), 168 } 169 return attributes 170 } 171 172 // Let's try to find all cycles by applying Johnson's method on the directed graph 173 // We cannot build a line graph and move out only rules because of hashicorp/terraform#11011 174 func findSgsToMoveOut(securityGroups []types.SecurityGroup) []string { 175 // Vertexes are security groups, edges are rules. The task is to find correct set of rule definitions, so that we 176 // won't have cycles 177 sourceGraph := simplegraph.NewDirectedGraph() 178 idToSg := make(map[int]types.SecurityGroup) 179 sgToIdx := make(map[string]int64) 180 for idx, sg := range securityGroups { 181 idToSg[idx] = sg 182 sgToIdx[StringValue(sg.GroupId)] = int64(idx) 183 sourceGraph.AddNode(sourceGraph.NewNode()) 184 } 185 for idx, sg := range securityGroups { 186 for _, rule := range sg.IpPermissions { 187 pairs := rule.UserIdGroupPairs 188 for _, pair := range pairs { 189 if pair.GroupId != nil { 190 fromNode := sourceGraph.Node(int64(idx)) 191 toNode := sourceGraph.Node(sgToIdx[StringValue(pair.GroupId)]) 192 if fromNode.ID() != toNode.ID() { 193 sourceGraph.SetEdge(sourceGraph.NewEdge(fromNode, toNode)) 194 } 195 } 196 } 197 } 198 } 199 200 cyclesInLineGraph := topo.DirectedCyclesIn(sourceGraph) // C1 cycles won't be found but Terraform solves that issue 201 resultingSet := make(map[string]void) 202 203 for _, v := range cyclesInLineGraph { 204 if elementAlreadyFound(resultingSet, v, idToSg) { 205 continue 206 } 207 208 // Try to move out node with lowest number of rules 209 group := idToSg[int(v[0].ID())] 210 for _, vi := range v { 211 viGroup := idToSg[int(vi.ID())] 212 if len(viGroup.IpPermissions) < len(group.IpPermissions) { 213 group = viGroup 214 } 215 } 216 217 resultingSet[*group.GroupId] = member 218 } 219 220 result := make([]string, len(resultingSet)) 221 i := 0 222 for k := range resultingSet { 223 result[i] = k 224 i++ 225 } 226 227 return result 228 } 229 230 func elementAlreadyFound(resultingSet map[string]void, v []graph.Node, idToSg map[int]types.SecurityGroup) bool { 231 for k := range resultingSet { 232 for _, vi := range v { 233 viGroupID := *idToSg[int(vi.ID())].GroupId 234 if k == viGroupID { 235 return true 236 } 237 } 238 } 239 return false 240 } 241 242 func (g *SecurityGenerator) InitResources() error { 243 config, err := g.generateConfig() 244 if err != nil { 245 return err 246 } 247 svc := ec2.NewFromConfig(config) 248 p := ec2.NewDescribeSecurityGroupsPaginator(svc, &ec2.DescribeSecurityGroupsInput{}) 249 var resourcesToFilter []types.SecurityGroup 250 for p.HasMorePages() { 251 page, err := p.NextPage(context.TODO()) 252 if err != nil { 253 return err 254 } 255 resourcesToFilter = append(resourcesToFilter, page.SecurityGroups...) 256 } 257 sort.Slice(resourcesToFilter, func(i, j int) bool { 258 return *resourcesToFilter[i].GroupId < *resourcesToFilter[j].GroupId 259 }) 260 g.Resources = g.createResources(resourcesToFilter) 261 262 return nil 263 } 264 265 func (g *SecurityGenerator) PostConvertHook() error { 266 for _, resource := range g.Resources { 267 if resource.InstanceInfo.Type == "aws_security_group_rule" { 268 if resource.Item["self"] == "true" { 269 delete(resource.Item, "source_security_group_id") 270 } 271 } else if resource.InstanceInfo.Type == "aws_security_group" { 272 if resource.Item["clearRules"] == true { 273 delete(resource.Item, "ingress") 274 delete(resource.Item, "egress") 275 delete(resource.Item, "clearRules") 276 continue 277 } 278 279 if val, ok := resource.Item["ingress"]; ok { 280 g.sortRules(val.([]interface{})) 281 } 282 if val, ok := resource.Item["egress"]; ok { 283 g.sortRules(val.([]interface{})) 284 } 285 } 286 } 287 return nil 288 } 289 290 func (g *SecurityGenerator) sortRules(rules []interface{}) { 291 for _, rule := range rules { 292 ruleMap := rule.(map[string]interface{}) 293 g.sortIfExist("cidr_blocks", ruleMap) 294 g.sortIfExist("ipv6_cidr_blocks", ruleMap) 295 g.sortIfExist("security_groups", ruleMap) 296 } 297 sort.Slice(rules, func(i, j int) bool { 298 return fmt.Sprintf("%v", rules[i]) < fmt.Sprintf("%v", rules[j]) 299 }) 300 } 301 302 func (g *SecurityGenerator) sortIfExist(attribute string, ruleMap map[string]interface{}) { 303 if val, ok := ruleMap[attribute]; ok { 304 sort.Slice(val.([]interface{}), func(i, j int) bool { 305 return val.([]interface{})[i].(string) < val.([]interface{})[j].(string) 306 }) 307 } 308 } 309 310 func permissionID(sgID, ruleType, groupID string, ip types.IpPermission) string { 311 var buf bytes.Buffer 312 buf.WriteString(fmt.Sprintf("%s_%s_%s_%d_%d_", sgID, ruleType, *ip.IpProtocol, fromPort(ip), toPort(ip))) 313 314 if len(ip.IpRanges) > 0 { 315 s := make([]string, len(ip.IpRanges)) 316 for i, r := range ip.IpRanges { 317 s[i] = *r.CidrIp 318 } 319 sort.Strings(s) 320 321 for _, v := range s { 322 buf.WriteString(fmt.Sprintf("%s_", v)) 323 } 324 } 325 326 if len(ip.Ipv6Ranges) > 0 { 327 s := make([]string, len(ip.Ipv6Ranges)) 328 for i, r := range ip.Ipv6Ranges { 329 s[i] = *r.CidrIpv6 330 } 331 sort.Strings(s) 332 333 for _, v := range s { 334 buf.WriteString(fmt.Sprintf("%s_", v)) 335 } 336 } 337 338 if len(ip.PrefixListIds) > 0 { 339 s := make([]string, len(ip.PrefixListIds)) 340 for i, pl := range ip.PrefixListIds { 341 s[i] = *pl.PrefixListId 342 } 343 sort.Strings(s) 344 345 for _, v := range s { 346 buf.WriteString(fmt.Sprintf("%s_", v)) 347 } 348 } 349 350 if groupID != "" { 351 buf.WriteString(fmt.Sprintf("%s_", groupID)) 352 } 353 354 idPreformatted := buf.String() 355 return idPreformatted[:len(idPreformatted)-1] 356 } 357 358 func fromPort(ip types.IpPermission) int { 359 switch { 360 case *ip.IpProtocol == "icmp": 361 return -1 362 case ip.FromPort > 0: 363 return int(ip.FromPort) 364 default: 365 return 0 366 } 367 } 368 369 func toPort(ip types.IpPermission) int { 370 switch { 371 case *ip.IpProtocol == "icmp": 372 return -1 373 case ip.ToPort > 0: 374 return int(ip.ToPort) 375 default: 376 return 65536 377 } 378 } 379 380 func ipRange(rule types.IpPermission) []string { 381 result := make([]string, len(rule.IpRanges)) 382 for idx, rule := range rule.IpRanges { 383 result[idx] = *rule.CidrIp 384 } 385 return result 386 } 387 388 func ip6Range(rule types.IpPermission) []string { 389 result := make([]string, len(rule.Ipv6Ranges)) 390 for idx, rule := range rule.Ipv6Ranges { 391 result[idx] = *rule.CidrIpv6 392 } 393 return result 394 } 395 396 func prefixes(rule types.IpPermission) []string { 397 result := make([]string, len(rule.PrefixListIds)) 398 for idx, rule := range rule.PrefixListIds { 399 result[idx] = *rule.PrefixListId 400 } 401 return result 402 }