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  }