Created
July 27, 2018 23:11
-
-
Save greglook/747ae5671074e2905225a5a3e1e710e8 to your computer and use it in GitHub Desktop.
Render Terraform AWS security group diffs for humans
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Better Terraform security group diffing. Feed the terraform resource diff | |
# output into this script's standard input. | |
SGR_ANSI = "\e[%sm" | |
SGR_CODES = { | |
none: 0, | |
bold: 1, | |
black: 30, | |
red: 31, | |
green: 32, | |
yellow: 33, | |
blue: 34, | |
magenta: 35, | |
cyan: 36, | |
white: 37, | |
} | |
# Constructs an ANSI Select Graphic Rendition sequence. | |
def sgr(codes) | |
SGR_ANSI % codes.map{|c| SGR_CODES[c] || raise("Unknown SGR code key: #{c.inspect}") }.join(';') | |
end | |
# Apply color codes to a section of text. | |
def color(text, *codes) | |
[sgr(codes), text, sgr([:none])].join('') | |
end | |
# Parse an input line into an attribute structure. | |
def parse_line(line) | |
if /^ *(\S+): +"([^"]*)" => "([^"]*)"$/ === line.chomp | |
{name: $1.split('.'), before: $2, after: $3} | |
else | |
STDERR.puts "Ignoring unknown line format: #{line.inspect}" | |
nil | |
end | |
end | |
rule_map = {} | |
# Process input. | |
while STDIN.gets do | |
line = parse_line($_) | |
name = line && line[:name] | |
if name && name[0] == 'ingress' | |
rule_id = name[1] | |
next if rule_id == '#' | |
rule_map[rule_id] ||= {} | |
rule_attr = name[2] | |
next if rule_attr == 'self' | |
if name.count > 3 | |
next if name[3] == '#' | |
rule_map[rule_id][rule_attr] ||= {before: [], after: []} | |
rule_map[rule_id][rule_attr][:before] << line[:before] unless line[:before] == '' | |
rule_map[rule_id][rule_attr][:after] << line[:after] unless line[:after] == '' | |
else | |
before = line[:before] != '' && line[:before] || nil | |
after = line[:after] != '' && line[:after] || nil | |
rule_map[rule_id][rule_attr] = {before: before, after: after} if before || after | |
end | |
end | |
end | |
rules = [] | |
BLANKS = [nil, '', '0', []] | |
def rule_unchanged?(rule) | |
rule.values.all? {|v| v[:before] == v[:after] } | |
end | |
def rule_added?(rule) | |
rule.all? {|k, v| k.is_a?(Symbol) || (BLANKS.include?(v[:before]) && v[:after]) } | |
end | |
def rule_removed?(rule) | |
rule.all? {|k, v| k.is_a?(Symbol) || (v[:before] && BLANKS.include?(v[:after])) } | |
end | |
def find_match(rule, rside, candidates, cside, *fields) | |
candidates.each do |candidate| | |
matched = true | |
fields.each do |field| | |
rval = rule[field] && rule[field][rside] | |
cval = candidate[field] && candidate[field][cside] | |
matched = false unless rval && cval && rval == cval | |
end | |
return candidate if matched | |
end | |
nil | |
end | |
# If all the attributes are the same, no change. Remove these from dedupe | |
# consideration. | |
rule_map.keys.each do |rule_id| | |
rule = rule_map[rule_id] | |
next unless rule_unchanged?(rule) | |
rule[:id] = {before: rule_id, after: rule_id} | |
rule[:state] = :unchanged | |
rules << rule | |
rule_map.delete(rule_id) | |
end | |
# Set ids so we can access later. | |
rule_map.each do |rule_id, rule| | |
rule[:id] = rule_id | |
end | |
# Try to pair up additions and removals. | |
rule_map.keys.each do |rule_id| | |
rule = rule_map[rule_id] | |
next unless rule | |
if rule_added? rule | |
candidates = rule_map.values.select {|r| rule_removed? r } | |
match = find_match(rule, :after, candidates, :before, 'description') \ | |
|| find_match(rule, :after, candidates, :before, 'protocol', 'from_port', 'to_port') \ | |
|| find_match(rule, :after, candidates, :before, 'cidr_blocks') \ | |
|| find_match(rule, :after, candidates, :before, 'ipv6_cidr_blocks') \ | |
|| find_match(rule, :after, candidates, :before, 'security_groups') | |
rule_map.delete(rule_id) | |
if match | |
rule_map.delete(match[:id]) | |
merged = {id: {before: match[:id], after: rule_id}} | |
(rule.keys + match.keys).uniq.each do |k| | |
next if k == :id | |
merged[k] = {before: match[k][:before], after: rule[k][:after]} | |
end | |
merged[:state] = :diff | |
rules << merged | |
else | |
rule[:id] = {after: rule_id} | |
rule[:state] = :added | |
rules << rule | |
end | |
elsif rule_removed? rule | |
candidates = rule_map.values.select {|r| rule_added? r } | |
match = find_match(rule, :before, candidates, :after, 'description') \ | |
|| find_match(rule, :before, candidates, :after, 'protocol', 'from_port', 'to_port') \ | |
|| find_match(rule, :before, candidates, :after, 'cidr_blocks') \ | |
|| find_match(rule, :before, candidates, :after, 'ipv6_cidr_blocks') \ | |
|| find_match(rule, :before, candidates, :after, 'security_groups') | |
rule_map.delete(rule_id) | |
if match | |
rule_map.delete(match[:id]) | |
merged = {id: {before: rule_id, after: match[:id]}} | |
(rule.keys + match.keys).uniq.each do |k| | |
next if k == :id | |
merged[k] = {before: rule[k][:before], after: match[k][:after]} | |
end | |
merged[:state] = :diff | |
rules << merged | |
else | |
rule[:id] = {before: rule_id} | |
rule[:state] = :removed | |
rules << rule | |
end | |
else | |
raise "Unknown rule changes: #{rule_id} => #{rule.inspect}" | |
end | |
end | |
# Roughly sort rules by port order. | |
rules.sort_by! do |rule| | |
from_port = rule['from_port'] | |
to_port = rule['to_port'] | |
(from_port && (from_port[:before] || from_port[:after])) \ | |
|| (to_port && (to_port[:before] || to_port[:after])) | |
end | |
# Return a human-readable string for the port range in a rule. | |
def port_range(rule, side) | |
protocol = rule['protocol'][side] || '???' | |
from_port = rule['from_port'][side] || '?' | |
to_port = rule['to_port'][side] || '?' | |
if protocol == 'icmp' && from_port == '-1' && to_port == '-1' | |
range = '(all)' | |
elsif from_port == '0' && to_port == '65535' | |
range = '(all)' | |
elsif from_port == to_port | |
range = from_port | |
else | |
range = "#{from_port}-#{to_port}" | |
end | |
"#{protocol.upcase} #{range}" | |
end | |
# Diff two lists and return colorized and indented text. | |
def diff_lists(left, right) | |
(left + right).uniq.sort.each do |val| | |
if left.include?(val) && right.include?(val) | |
puts " #{color(val, :cyan)}" | |
elsif left.include?(val) | |
puts " #{color(val, :red)}" | |
else | |
puts " #{color(val, :green)}" | |
end | |
end | |
end | |
special_attrs = ['description', 'protocol', 'from_port', 'to_port'] | |
list_attrs = ['cidr_blocks', 'ipv6_cidr_blocks', 'security_groups'] | |
# Pretty print each rule change. | |
rules.each do |rule| | |
case rule[:state] | |
when :unchanged | |
puts color(rule[:id][:before], :bold, :cyan) | |
puts " #{rule['description'][:before]}" if rule['description'] | |
puts " #{port_range(rule, :before)}" | |
when :removed | |
puts "#{color(rule[:id][:before], :bold, :red)}" | |
puts " #{rule['description'][:before]}" if rule['description'] | |
puts " #{port_range(rule, :before)}" | |
when :added | |
puts "#{color(rule[:id][:after], :bold, :green)}" | |
puts " #{rule['description'][:after]}" if rule['description'] | |
puts " #{port_range(rule, :after)}" | |
else | |
puts "#{color(rule[:id][:before], :bold, :yellow)} => #{color(rule[:id][:after], :bold, :yellow)}" | |
description = rule['description'] | |
if description && description[:before] == description[:after] | |
puts description[:before] | |
elsif description | |
puts " #{description[:before].inspect} => #{description[:after].inspect}" | |
end | |
before_range = port_range(rule, :before) | |
after_range = port_range(rule, :after) | |
if before_range == after_range | |
puts " #{before_range}" | |
else | |
puts " #{port_range(rule, :before)} => #{port_range(rule, :after)}" | |
end | |
end | |
list_attrs.each do |k| | |
next unless rule[k] | |
before = rule[k][:before] | |
after = rule[k][:after] | |
puts " #{k}" | |
diff_lists before, after | |
end | |
rule.each do |k, v| | |
next if special_attrs.include?(k) || list_attrs.include?(k) || k.is_a?(Symbol) | |
before = v[:before] | |
after = v[:after] | |
if before == after | |
puts " #{k}: #{before}" | |
else | |
puts " #{k}: #{before} => #{after}" | |
end | |
end | |
end |
Can you update it with egress block as well, and it only supports terraform 11!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Turns this:
into this: