Skip to content

Instantly share code, notes, and snippets.

@alexmherrmann
Created March 9, 2019 09:57
Show Gist options
  • Save alexmherrmann/8f683ccbf732ea720d08f95f3a5ec857 to your computer and use it in GitHub Desktop.
Save alexmherrmann/8f683ccbf732ea720d08f95f3a5ec857 to your computer and use it in GitHub Desktop.
Elastic Beanstalk terraform output parser

If you've ever created an elastic beanstalk environment in terraform and tried to change just a single setting on the environment you'll know why I made this. Basically it takes the output of the plan command that looks like this:

...
      setting.2207777926.name:      "StreamLogs" => "StreamLogs"
      setting.2207777926.namespace: "aws:elasticbeanstalk:cloudwatch:logs" => "aws:elasticbeanstalk:cloudwatch:logs"
      setting.2207777926.resource:  "" => ""
      setting.2207777926.value:     "false" => "false"
      setting.2262995125.name:      "Availability Zones" => "Availability Zones"
      setting.2262995125.namespace: "aws:autoscaling:asg" => "aws:autoscaling:asg"
      setting.2262995125.resource:  "" => ""
      setting.2262995125.value:     "Any 2" => "Any 2"
      setting.2276893638.name:      "RollingUpdateType" => "RollingUpdateType"
      setting.2276893638.namespace: "aws:autoscaling:updatepolicy:rollingupdate" => "aws:autoscaling:updatepolicy:rollingupdate"
      setting.2276893638.resource:  "" => ""
      setting.2276893638.value:     "Health" => "Health"
...

And turns it into something more like this aws:autoscaling:launchconfiguration/InstanceType: t2.micro -> t3.small. Printing only the actual changes made to the environment.

It's a little ugly but it works well enough. Will log out what it's thinking to a file called err.log.

package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
)
// FromTo is A little class that lets you check to see if two stored values are different
type fromTo [2]string
func createFromTo(from, to string) fromTo {
return [2]string{from, to}
}
func (f *fromTo) getFrom() string {
return f[0]
}
func (f *fromTo) getTo() string {
return f[1]
}
func (f *fromTo) checkDifferent() bool {
return f.getFrom() != f.getTo()
}
func (f *fromTo) getOneIfEmpty() string {
// if we have different values, and one is empty, return the other
if f.checkDifferent() {
if f.getFrom() == "" {
return f.getTo()
}
return f.getFrom()
}
return f.getFrom()
}
func (f *fromTo) setBothIgnoreEmpty(from, to string) {
if f == nil {
f = new(fromTo)
}
if from != "" {
f[0] = from
}
if to != "" {
f[1] = to
}
}
var errlog *log.Logger
func init() {
var logFile, err = os.OpenFile("err.log", os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0644)
if err != nil {
panic(fmt.Sprintf("Couldn't open file err.log!!\n%s\n", err.Error()))
}
errlog = log.New(logFile, "tfeb> ", log.Ltime)
}
// You can put a little snippet here and uncomment the matching code in main to use this
const test = ``
type setting struct {
Name *fromTo
Namespace *fromTo
Resource *fromTo
Value *fromTo
}
func (s *setting) doDisplay() bool {
return (s.Value != nil && s.Value.checkDifferent()) || (s.Resource != nil && s.Resource.checkDifferent())
}
// Take key (name, namespace, etc) and populate the struct with it
func (s *setting) populateWith(key string, val fromTo) {
switch (key) {
case "name":
s.Name = new(fromTo)
*s.Name = val
break;
case "namespace":
s.Namespace = new(fromTo)
*s.Namespace = val
break;
case "value":
s.Value = new(fromTo)
*s.Value = val
break;
case "resource":
s.Resource = new(fromTo)
*s.Resource = val
break;
}
}
// var valuePuller = regexp.MustCompile(`^setting.(\d+).(\w+):\s*"([\w ]*)" => "([\w ]*)"$`)
var valuePuller = regexp.MustCompile(`\s*setting\.(\d+)\.(name|namespace|value|resource):\s*(?:"(.*)" => )?"(.*)"`)
func main() {
//stdin, err := ioutil.ReadAll(os.Stdin)
if len(os.Args) != 2 {
errlog.Println("Need a file")
}
stdin, err := ioutil.ReadFile(os.Args[1])
if err != nil {
errlog.Println("no stdin!")
os.Exit(-1)
}
lines := strings.Split(string(stdin), "\n")
//NOTE: Change back when not testing
//lines = strings.Split(test, "\n")
settingMap := parseLinesIntoMap(lines)
errlog.Println("~~~Now starting to parse out all the changes")
diff := getChanges(settingMap)
for k, v := range diff {
fmt.Printf("%s: %s\n", k, v)
}
//for _, v := range settingMap {
// if v != nil && v.doDisplay() {
// if mBytes, err := json.MarshalIndent(v, "", " "); err == nil {
// fmt.Println(string(mBytes))
// } else {
// errlog.Println(mBytes)
// }
// }
//}
}
type change struct {
Name string
Namespace string
Value fromTo
}
type changes map[string]string
func noneNil(pointers ...*fromTo) bool {
for _, p := range pointers {
if p == nil {
return false
}
}
return true
}
func getChanges(settings map[string]*setting) changes {
// Map of Name,Namespace -> Value
namesSpaceNameMap := make(map[[2]string]*fromTo)
for _, v := range settings {
if noneNil(v.Namespace, v.Name, v.Value) {
key := [2]string{v.Name.getOneIfEmpty(), v.Namespace.getOneIfEmpty()}
valueFT, ok := namesSpaceNameMap[key]
if !ok || valueFT == nil {
valueFT = new(fromTo)
}
valueFT.setBothIgnoreEmpty(v.Value.getFrom(), v.Value.getTo())
namesSpaceNameMap[key] = valueFT
}
}
var result changes = make(changes)
for k, v := range namesSpaceNameMap {
// namespace/name = from -> to
if v.checkDifferent() {
result[fmt.Sprintf("%s/%s", k[1], k[0])] = fmt.Sprintf("%s -> %s", v.getFrom(), v.getTo())
}
}
return result
}
func parseLinesIntoMap(lines []string) map[string]*setting {
settingMap := make(map[string]*setting)
for _, line := range lines {
matches := valuePuller.FindStringSubmatch(line)
const printMat = "%-15s: [%s]\n"
if matches == nil || len(matches) < 4 {
errlog.Printf(printMat, "not a match", line)
continue
} else {
errlog.Printf(printMat, "match!", line)
}
var found *setting
if val, ok := settingMap[matches[1]]; ok && val != nil {
found = val
} else {
found = new(setting)
settingMap[matches[1]] = found
}
if len(matches) == 5 {
found.populateWith(matches[2], createFromTo(matches[3], matches[4]))
} else if len(matches) == 4 {
found.populateWith(matches[2], createFromTo("", matches[3]))
} else {
errlog.Printf("incorrect number of matches: [%s]\n", line)
continue
}
}
return settingMap
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment