diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5afaf7d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+/build
+/vendor
+.idea
+*.iml
+.terraform/
+.DS_Store
+*.plan
+*.tfvars
+*.tfstate
+*.code-workspace
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..fc3bc80
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+# Contributing
+
+Thank you for considering contributing to this project!
+
+## Contribution steps
+1. If you need to push a trivial change, go ahead, and create a PR.
+2. For bigger changes, it's generally best to open an issue describing the bug or feature request you are intending to solve.
+3. To avoid any duplicated work, claim any open issue by putting your name in the body of the issue and mention that you want to take this up; in case there is no open issue yet, open one and claim it (or not).
+
+## Pull Request Checklist
+1. Fork this repository, create a feature branch, and when you are done subsequently create an upstream PR.
+2. Make sure you leave an good explaintatory description so people understand the what/why, and how
+3. Make sure your PR passes all the tests
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5968baa
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+NAME := klaabu
+
+BUILD := ${CURDIR}/build
+BIN := ${BUILD}/bin/${NAME}
+
+.PHONY: clean tidy compile test run
+.DEFAULT_GOAL := build
+
+clean:
+ rm -rf ${BUILD}
+
+tidy:
+ go mod tidy -v
+
+fmt:
+ go fmt github.com/erikkn/klaabu/...
+
+vendor:
+ go mod vendor
+
+compile:
+ go build -mod=readonly -o ${BIN} cli/*.go
+
+test:
+ go test -v ./...
+
+build: tidy fmt vendor compile
+
+run: build
+ ${BIN}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..46279c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+# Klaabu
+
+Klaabu is an IP network documenting tool, usually referred to as an IPAM (IP Address Management) tool, with a very strong focus on simplicity, convenience, and operating in Cloud-Native environments.
+
+## Preface
+A big THANKS to **Taras Burko**, for mentoring me, and for all your time & effort in writing this program with me.
+
+A big THANKS to **Taavi Tuisk** for mentoring & helping me these past years.
+
+## Why invent something new?
+There are a ton of different IPAM tools available out there, but with Klaabu you don’t have to host and maintain yet another tool. NetBox, for instance, is a great tool, but in case NetBox is your source-of-truth, you will need to have some sort of an SLA, strong observability, DR plan, and more.
+Klaabu tries to solve this unnecessary TOIL by using a file-based schema that you can store in Git and a powerful `validate` function to make sure your schema is in the right state. The validate function can be used in your CI pipeline, or standalone, to make sure you don’t have duplicated, overlapping CIDRs, or some other schema mistakes. The combination of storing your schema in Git and the validate function offers exactly the kind of workflow you are already used to, with code reviews, audit trails, and more.
+
+### Why another markup language?
+The first couple of iterations of the Klaabu program were using a YAML-based schema. However, after using Klaabu in a live environment (with 100 VPCs, ~500 subnets, BGP ASNs, and more) we decided that this is not the right format. Using a YAML-based schema in such an environment will result in hundreds of lines, which becomes very hard to read. We believe that the ‘Klaabu Markup Language’ (`kml`) is much easier and more convenient to the user.
+
+### Terraform
+Apart from some other powerful (CLI) functions, Klaabu offers the `export-terraform` function which exports your schema to a Terraform supported module. In turn, you can import this module from any other Terraform module and lookup the CIDRs, attributes, or labels. Having a single source-of-truth that also works natively with Terraform is very convenient and will simplify the usage of your VPC, Subnet, SG, and other Terraform modules a lot.
+
+## Installation
+With GO installed:
+
+```bash
+go get github.com/erikkn/klaabu
+```
+
+Alternatively, you can also import the package directly:
+
+```bash
+import “github.com/erikkn/klaabu”
+```
+
+In case you want to build the package yourself
+
+```bash
+make build
+```
+
+Last but not least, you can also just download the binary directly from the release page.
+
+## CLI usage
+```
+Usage: klaabu [args]
+```
+
+At the moment the Klaabu CLI supports the following commands:
+* `find`: recursively search the schema for any object that matches your search pattern.
+* `get`: in contrast to the `find` command, `get` retrieves a single object in the schema.
+* `list`: the `list` command shows all the child objects of a certain instance.
+* `space`: use this command to see the available IP space within a certain prefix/object.
+* `init`: initializes a new schema.
+* `validate`: validates your schema, including the actual content of the objects (e.g. valid CIDR, no overlapping CIDRs, and more).
+* `fmt`: used to rewrite Klaabu configuration files to a canonical format and style.
+* `export-terraform`: exports your schema to a valid Terraform module; your schema is stored in a file with the `tf.json` notation, which is a valid input module for Terraform.
+
+### Examples
+The examples assume your schema lives in the current working directory / you have an environment variable set pointing to the location of your schema.
+
+```bash
+klaabu space 192.168.0.0/20
+```
+
+```bash
+klaabu find -label az=euc1-az1
+
+klaabu find -label vpc=foobar,env=production
+```
+
+## Workflow
+- Create a new private repository and store your `schema.kml` in there
+- Use your IDE to add a new CIDR to your schema
+- Run `klaabu fmt` to produce configuration files that conform to the imposed style
+- Run `klaabu validate` to make sure your schema is in a valid state and that you don’t have overlapping CIDRs for instance
+- Follow your personal/company’s process for committing and merging your changes. You probably want to follow the traditional code review process with a CI pipeline that also uses the validate function.
+
+## Contributing
+Check out the [CONTRIBUTING](./CONTRIBUTING.md/) guide if you want to contribute.
diff --git a/cli/cli b/cli/cli
new file mode 100755
index 0000000..2d97c16
Binary files /dev/null and b/cli/cli differ
diff --git a/cli/find.go b/cli/find.go
new file mode 100644
index 0000000..ca4a7d8
--- /dev/null
+++ b/cli/find.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/erikkn/klaabu/klaabu"
+)
+
+// The purpose of the 'find' command is more to 'explore' the schema and searching for something in a broad way. 'GET', however, is used if you already know what you need to lookup and do a specific search.
+
+func findCommand() {
+ find := flag.NewFlagSet("find", flag.ExitOnError)
+
+ schemaFileName := find.String("schema", "schema.kml", "(Optional) Schema file path (default './schema.kml')")
+ termsString := find.String("label", "", "(Optional) Filter by labels. Example: 'az=euc1-az1,type=vpc'. No filtering when empty.")
+ err := find.Parse(os.Args[2:])
+
+ parentId := find.Arg(0)
+
+ if err != nil || len(os.Args) < 2 {
+ log.Printf("Usage: klaabu find [OPTIONS] [PARENT_ID] \n\n Subcommands: \n")
+ find.PrintDefaults()
+ os.Exit(1)
+ }
+
+ schema, err := klaabu.LoadSchemaFromKmlFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ var parent *klaabu.Prefix
+ if parentId == "" {
+ parent = schema.Root
+ } else {
+ parent = schema.PrefixById(parentId)
+ if parent == nil {
+ log.Fatalln(err)
+ }
+ }
+
+ terms := make([]*klaabu.LabelSearchTerm, 0)
+
+ if *termsString != "" {
+ termStrings := strings.Split(*termsString, ",")
+ for _, termString := range termStrings {
+ termSlice := strings.Split(termString, "=")
+ var term klaabu.LabelSearchTerm
+ if len(termSlice) == 2 {
+ term.Key = strings.TrimSpace(termSlice[0])
+ value := strings.TrimSpace(termSlice[1])
+ term.Value = &value
+ } else if len(termSlice) == 1 {
+ term.Key = strings.TrimSpace(termSlice[0])
+ term.Value = nil
+ }
+ terms = append(terms, &term)
+ }
+ }
+
+ println(">>> num terms: ", len(terms))
+ for _, term := range terms {
+ println(">>> term: ", term.Key, term.Value)
+ }
+
+ cidrs := parent.FindPrefixesByLabelTerms(terms)
+
+ for _, v := range cidrs {
+ labels := make([]string, 0, len(v.Labels))
+ for k, v := range v.Labels {
+ labels = append(labels, k+"="+v)
+ }
+
+ log.Printf("%s: %s [%s]\n", string(v.Cidr), strings.Join(v.Aliases, "|"), strings.Join(labels, ","))
+ }
+}
diff --git a/cli/fmt.go b/cli/fmt.go
new file mode 100644
index 0000000..1758ac8
--- /dev/null
+++ b/cli/fmt.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "flag"
+ "io/ioutil"
+ "log"
+ "os"
+
+ "github.com/erikkn/klaabu/klaabu"
+)
+
+func fmtCommand() {
+ flags := flag.NewFlagSet("fmt", flag.ExitOnError)
+
+ schemaFileName := flags.String("schema", "schema.kml", "Schema file path")
+ flags.Parse(os.Args[2:])
+
+ node, err := klaabu.LoadKmlFromFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ schema, err := klaabu.KmlToSchema(node)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ err = schema.Validate()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ originalBytes, err := ioutil.ReadFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ wf, err := os.OpenFile(*schemaFileName, os.O_WRONLY, 0)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ err = klaabu.MarshalKml(node, wf)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ updatedBytes, err := ioutil.ReadFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if string(originalBytes) != string(updatedBytes) {
+ log.Println(*schemaFileName)
+ }
+}
diff --git a/cli/get.go b/cli/get.go
new file mode 100644
index 0000000..88bc2d4
--- /dev/null
+++ b/cli/get.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "flag"
+ "github.com/erikkn/klaabu/klaabu"
+ "log"
+ "os"
+)
+
+func getCommand() {
+ get := flag.NewFlagSet("get", flag.ExitOnError)
+ schemaFileName := get.String("schema", "schema.kml", "(Optional) Schema file path (defaults to ./schema.kml)")
+ err := get.Parse(os.Args[2:])
+
+ prefixId := get.Arg(0)
+
+ if err != nil || len(os.Args) < 3 {
+ log.Printf("Usage: klaabu get [OPTIONS] PREFIX_ID \n\n Subcommands: \n")
+ get.PrintDefaults()
+ os.Exit(1)
+ }
+
+ schema, err := klaabu.LoadSchemaFromKmlFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ prefix := schema.PrefixById(prefixId)
+ if prefix == nil {
+ log.Fatalf("not found: %s", prefixId)
+ }
+
+ // TODO: Output all the individual fields, might change again so a `todo` for later.
+ log.Println(prefix)
+}
diff --git a/cli/init.go b/cli/init.go
new file mode 100644
index 0000000..8e6f429
--- /dev/null
+++ b/cli/init.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/erikkn/klaabu/klaabu"
+)
+
+func initCommand() {
+ init := flag.NewFlagSet("init", flag.ExitOnError)
+
+ // TODO change to schema.kml
+ schemaFileName := init.String("schema", "schema-new.kml", "Schema file path")
+ version := init.String("version", "v1", "Schema version")
+ labelsString := init.String("labels", "", "(Optional) Schema labels, e.g. 'org=acme,env=staging'")
+
+ init.Parse(os.Args[2:])
+
+ if len(os.Args) < 3 {
+ log.Printf("Usage: klaabu init [OPTIONS] \n\n Subcommands: \n")
+ init.PrintDefaults()
+ os.Exit(1)
+ }
+
+ if *version != "v1" {
+ log.Printf("%s is not a valid schema version \n\n", *version)
+ log.Printf("Usage: klaabu init [OPTIONS] \n\n Subcommands: \n")
+ init.PrintDefaults()
+ }
+
+ labels := make(map[string]string)
+ if *labelsString != "" {
+ for _, v := range strings.Split(*labelsString, ",") {
+ pair := strings.Split(v, "=")
+ if len(pair) != 2 {
+ log.Fatalln("error with labels")
+ }
+
+ labels[strings.TrimSpace(pair[0])] = strings.TrimSpace(pair[1])
+ }
+ }
+
+ schema := klaabu.NewSchema(labels)
+
+ err := klaabu.WriteSchemaToFile(schema, schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ log.Printf("Successfully created your new schema '%s' \n", *schemaFileName)
+}
diff --git a/cli/main.go b/cli/main.go
new file mode 100644
index 0000000..8ea3f2d
--- /dev/null
+++ b/cli/main.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "log"
+ "os"
+)
+
+var commands = map[string]func(){
+ "export-terraform": exportTerraformCommand,
+ "find": findCommand,
+ "fmt": fmtCommand,
+ "get": getCommand,
+ "init": initCommand,
+ "space": spaceCommand,
+ "validate": validateCommand,
+}
+
+func helpCommand(c string) {
+ log.Printf("Usage: klaabu COMMAND [OPTIONS] [ARGS] \n\n Common commands: ")
+ for key := range commands {
+ log.Printf(" %s \n", key)
+ }
+ os.Exit(1)
+}
+
+func main() {
+ // Remove the timestamp prefix
+ log.SetFlags(0)
+
+ if len(os.Args) < 2 {
+ helpCommand("")
+ }
+
+ commandName := os.Args[1]
+ c, ok := commands[commandName]
+ if !ok {
+ helpCommand(commandName)
+ }
+
+ c()
+}
diff --git a/cli/space.go b/cli/space.go
new file mode 100644
index 0000000..7c02476
--- /dev/null
+++ b/cli/space.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+ "flag"
+ "github.com/erikkn/klaabu/klaabu"
+ "github.com/erikkn/klaabu/klaabu/iputil"
+ "log"
+ "os"
+ "sort"
+)
+
+func spaceCommand() {
+ flagSet := flag.NewFlagSet("space", flag.ExitOnError)
+
+ schemaFileName := flagSet.String("schema", "schema.kml", "(Optional) Schema file path (default './schema.kml')")
+
+ err := flagSet.Parse(os.Args[2:])
+
+ parentId := flagSet.Arg(0)
+
+ if err != nil || len(os.Args) < 2 {
+ log.Printf("Usage: klaabu space [OPTIONS] [PARENT_ID] \n\n Subcommands: \n")
+ flagSet.PrintDefaults()
+ os.Exit(1)
+ }
+
+ schema, err := klaabu.LoadSchemaFromKmlFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ var parent *klaabu.Prefix
+ if parentId == "" {
+ parent = schema.Root
+ } else {
+ parent = schema.PrefixById(parentId)
+ if parent == nil {
+ log.Fatalf("Parent not found: %s", parentId)
+ }
+ }
+
+ min, max, err := iputil.MinMaxIP(string(parent.Cidr))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ childCidrs := make([]klaabu.Cidr, 0, len(parent.Children))
+ for _, child := range parent.Children {
+ childCidrs = append(childCidrs, child.Cidr)
+ }
+
+ sort.Slice(childCidrs, func(i1, i2 int) bool {
+ min1, _, _ := iputil.MinMaxIP(string(childCidrs[i1]))
+ min2, _, _ := iputil.MinMaxIP(string(childCidrs[i2]))
+ cmp, _ := iputil.CompareIPs(min1, min2)
+ return cmp < 0
+ })
+
+ for _, cidr := range childCidrs {
+ childMin, childMax, err := iputil.MinMaxIP(string(cidr))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ childMinCmp, err := iputil.CompareIPs(min, childMin)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ childStartsAtMin := childMinCmp == 0
+
+ if !childStartsAtMin {
+ // detected a gap between min and the child start, grab it
+ childStartMinusOne, err := iputil.PreviousIP(childMin)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Printf("From: %s, To: %s", min, childStartMinusOne)
+ }
+
+ // continue right after the child ends
+ min, err = iputil.NextIP(childMax)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ minMaxCmp, err := iputil.CompareIPs(min, max)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if minMaxCmp < 0 {
+ // have some IP space remaining
+ log.Printf("From: %s, To: %s", min, max)
+ }
+}
diff --git a/cli/terraform.go b/cli/terraform.go
new file mode 100644
index 0000000..4c380c0
--- /dev/null
+++ b/cli/terraform.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/erikkn/klaabu/klaabu"
+ "github.com/erikkn/klaabu/klaabu/terraform"
+)
+
+func exportTerraformCommand() {
+ export := flag.NewFlagSet("export-terraform", flag.ExitOnError)
+
+ schemaFileName := export.String("schema", "schema.kml", "Schema file path")
+ export.Parse(os.Args[2:])
+
+ if len(os.Args) < 2 {
+ log.Printf("Usage: klaabu export-terraform [OPTIONS] \n\n Subcommands: \n")
+ export.PrintDefaults()
+ os.Exit(1)
+ }
+
+ schema, err := klaabu.LoadSchemaFromKmlFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ terraformJson, err := terraform.Generate(schema)
+ if err != nil {
+ log.Fatalf("error while generating JSON of your schema with error message: %s \n", err)
+ }
+
+ fmt.Println(string(terraformJson))
+ //log.Println(string(terraformJson))
+
+}
diff --git a/cli/validate.go b/cli/validate.go
new file mode 100644
index 0000000..2e53b21
--- /dev/null
+++ b/cli/validate.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+
+ "github.com/erikkn/klaabu/klaabu"
+)
+
+func validateCommand() {
+ validateFlags := flag.NewFlagSet("validate", flag.ExitOnError)
+
+ schemaFileName := validateFlags.String("schema", "schema.kml", "Schema file path")
+ validateFlags.Parse(os.Args[2:])
+
+ schema, err := klaabu.LoadSchemaFromKmlFile(*schemaFileName)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ err = schema.Validate()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ log.Println("Validation successful")
+}
diff --git a/examples/terraform/klaabu/outputs.tf.json b/examples/terraform/klaabu/outputs.tf.json
new file mode 100644
index 0000000..b2bd2a9
--- /dev/null
+++ b/examples/terraform/klaabu/outputs.tf.json
@@ -0,0 +1,15 @@
+{
+ "output": {
+ "aliases": {
+ "value": {
+ "aws": {
+ "cidr": "10.0.0.0/8",
+ "labels": {
+ "env": "prod",
+ "site": "aws"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/terraform/main.tf b/examples/terraform/main.tf
new file mode 100644
index 0000000..aaa074d
--- /dev/null
+++ b/examples/terraform/main.tf
@@ -0,0 +1,7 @@
+module "klaabu" {
+ source = "./klaabu"
+}
+
+output "aws" {
+ value = module.klaabu.aliases["aws"]
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..38951a5
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/erikkn/klaabu
+
+go 1.15
diff --git a/klaabu/iputil/ip.go b/klaabu/iputil/ip.go
new file mode 100644
index 0000000..b4eb93e
--- /dev/null
+++ b/klaabu/iputil/ip.go
@@ -0,0 +1,89 @@
+package iputil
+
+import (
+ "errors"
+ "fmt"
+ "net"
+)
+
+// CompareIPs Useful for sorting. CompareIPs(10.0.0.0/8, 10.0.0.1/8) == -1
+func CompareIPs(x, y net.IP) (int, error) {
+ if len(x) != len(y) {
+ return 0, fmt.Errorf("length of IP %v is different from %v , meaning two invalid IPs", x, y)
+ }
+
+ for i := 0; i < len(x); i++ {
+ if x[i] < y[i] {
+ return -1, nil
+ } else if x[i] > y[i] {
+ return 1, nil
+ }
+ }
+
+ return 0, nil
+}
+
+// MinMaxIP first and last IP of the CIDR range
+func MinMaxIP(cidr string) (net.IP, net.IP, error) {
+ _, ipNet, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error while parsing your CIDR %v with error: %s", cidr, err)
+ }
+
+ min := make([]byte, len(ipNet.IP))
+ max := make([]byte, len(ipNet.IP))
+ for i := range ipNet.IP {
+ min[i] = ipNet.Mask[i] & ipNet.IP[i]
+ max[i] = ipNet.Mask[i]&ipNet.IP[i] | ^ipNet.Mask[i]
+ }
+
+ return min, max, nil
+}
+
+func NextIP(ip net.IP) (net.IP, error) {
+ result := CloneIP(ip)
+
+ for i := len(result) - 1; i >= 0; i-- {
+ if i == 0 && result[i] == 255 {
+ return nil, errors.New("unable to increment max IP")
+ }
+
+ if result[i] < 255 {
+ result[i]++
+ break
+ } else {
+ result[i] = 0
+ }
+ }
+
+ return result, nil
+}
+
+func PreviousIP(ip net.IP) (net.IP, error) {
+ result := CloneIP(ip)
+
+ for i := len(result) - 1; i >= 0; i-- {
+ if i == 0 && result[i] == 0 {
+ return nil, errors.New("unable to decrement min IP")
+ }
+
+ if result[i] > 0 {
+ // no underflow, terminate
+ result[i]--
+ break
+ } else {
+ result[i] = 255
+ }
+ }
+
+ return result, nil
+}
+
+func CloneIP(ip net.IP) net.IP {
+ result := net.IP(make([]byte, len(ip)))
+ for index, b := range ip {
+ result[index] = b
+ }
+
+ return result
+}
diff --git a/klaabu/iputil/ip_test.go b/klaabu/iputil/ip_test.go
new file mode 100644
index 0000000..b650cb8
--- /dev/null
+++ b/klaabu/iputil/ip_test.go
@@ -0,0 +1,86 @@
+package iputil
+
+import (
+ "net"
+ "testing"
+)
+
+func Test_CloneIP(t *testing.T) {
+ ip := net.IPv4(1, 2, 3, 4)
+ clone := CloneIP(ip)
+ if !clone.Equal(ip) {
+ t.Fatalf("Source and clone not equal")
+ }
+ clone[0] = 5
+ if clone.Equal(ip) {
+ t.Fatalf("Source and clone share backing array")
+ }
+}
+
+func Test_NextIP(t *testing.T) {
+ type Case struct {
+ ip net.IP
+ next net.IP
+ fail bool
+ }
+
+ cases := []Case{
+ {net.IPv4(1, 2, 3, 4), net.IPv4(1, 2, 3, 5), false},
+ {net.IPv4(1, 2, 3, 255), net.IPv4(1, 2, 4, 0), false},
+ {net.IPv4(1, 2, 255, 255), net.IPv4(1, 3, 0, 0), false},
+ {net.IPv4(1, 255, 255, 255), net.IPv4(2, 0, 0, 0), false},
+ {net.IPv4(255, 255, 255, 255), net.IPv4(0, 0, 0, 0), true},
+ }
+
+ for _, c := range cases {
+ ip := c.ip.To4()
+ next, err := NextIP(ip)
+ if err != nil {
+ if !c.fail {
+ t.Errorf("Unexpected error: %s: %s", c.ip, err)
+ }
+ } else {
+ if c.fail {
+ t.Errorf("Expected error, but did not get any: %s: next = %s", c.ip, next)
+ } else {
+ if !next.Equal(c.next) {
+ t.Errorf("Expectation mismatch: %s: expected %s, got %s", c.ip, c.next, next)
+ }
+ }
+ }
+ }
+}
+
+func Test_PreviousIP(t *testing.T) {
+ type Case struct {
+ ip net.IP
+ next net.IP
+ fail bool
+ }
+
+ cases := []Case{
+ {net.IPv4(1, 2, 3, 4), net.IPv4(1, 2, 3, 3), false},
+ {net.IPv4(1, 2, 3, 0), net.IPv4(1, 2, 2, 255), false},
+ {net.IPv4(1, 2, 0, 0), net.IPv4(1, 1, 255, 255), false},
+ {net.IPv4(1, 0, 0, 0), net.IPv4(0, 255, 255, 255), false},
+ {net.IPv4(0, 0, 0, 0), net.IPv4(0, 0, 0, 0), true},
+ }
+
+ for _, c := range cases {
+ ip := c.ip.To4()
+ next, err := PreviousIP(ip)
+ if err != nil {
+ if !c.fail {
+ t.Errorf("Unexpected error: %s: %s", c.ip, err)
+ }
+ } else {
+ if c.fail {
+ t.Errorf("Expected error, but did not get any: %s: next = %s", c.ip, next)
+ } else {
+ if !next.Equal(c.next) {
+ t.Errorf("Expectation mismatch: %s: expected %s, got %s", c.ip, c.next, next)
+ }
+ }
+ }
+ }
+}
diff --git a/klaabu/kml.go b/klaabu/kml.go
new file mode 100644
index 0000000..db6fa55
--- /dev/null
+++ b/klaabu/kml.go
@@ -0,0 +1,145 @@
+package klaabu
+
+import (
+ "errors"
+ "fmt"
+ "github.com/erikkn/klaabu/klaabu/kml"
+ "io"
+ "os"
+)
+
+const (
+ cidrs = "cidrs"
+ universe = "0.0.0.0/0"
+)
+
+func MarshalKml(node *kml.Node, w io.Writer) error {
+ for _, child := range node.Children {
+ if child.Key == universe {
+ child.Key = cidrs
+ }
+ }
+
+ err := node.Marshal(w)
+ if err != nil {
+ return err
+ }
+
+ for _, child := range node.Children {
+ if child.Key == cidrs {
+ child.Key = universe
+ }
+ }
+
+ return nil
+}
+
+// LoadSchemaFromKmlFile parse Schema out of KML file.
+func LoadSchemaFromKmlFile(source string) (*Schema, error) {
+ node, err := LoadKmlFromFile(source)
+ if err != nil {
+ return nil, err
+ }
+
+ return KmlToSchema(node)
+}
+
+func LoadKmlFromFile(source string) (*kml.Node, error) {
+ f, err := os.Open(source)
+ defer f.Close()
+ if err != nil {
+ return nil, fmt.Errorf("error opening file: %s", err)
+ }
+
+ return kml.Parse(f)
+}
+
+func KmlToSchema(rootNode *kml.Node) (*Schema, error) {
+ topLevelNodes := make(map[string]*kml.Node)
+ for _, node := range rootNode.Children {
+ if topLevelNodes[node.Key] != nil {
+ return nil, fmt.Errorf("duplicate node: '%s' at line %d", node.Key, node.LineNum)
+ }
+
+ topLevelNodes[node.Key] = node
+ }
+
+ schemaNode := topLevelNodes["schema"]
+ if schemaNode == nil {
+ return nil, errors.New("'schema' node not found")
+ }
+ schemaVersion := schemaNode.Attributes["version"]
+ if schemaVersion != "v1" {
+ return nil, fmt.Errorf("unsupported schema version: %s", schemaVersion)
+ }
+
+ rootPrefixNode := topLevelNodes[cidrs]
+ if rootPrefixNode == nil {
+ return nil, errors.New("'cidrs' node not found")
+ }
+
+ err := detectAliasDuplicates(rootPrefixNode, make(map[string]*kml.Node))
+ if err != nil {
+ return nil, err
+ }
+
+ schema := NewSchema(schemaNode.Attributes)
+
+ rootPrefixNode.Key = universe
+ rootPrefix, err := prefixFromKmlNode(rootPrefixNode, nil)
+ if err != nil {
+ return nil, err
+ }
+ schema.Root = rootPrefix
+
+ return schema, nil
+}
+
+func detectAliasDuplicates(node *kml.Node, nodesByAlias map[string]*kml.Node) error {
+ for _, alias := range node.Aliases {
+ knownNode := nodesByAlias[alias]
+ if knownNode != nil {
+ return fmt.Errorf("duplicate alias: %s for %s (line %d) and %s (line %d). TIP: use -auxN suffix when provisioning additional VPC CIDRs", alias, knownNode.Key, knownNode.LineNum, node.Key, node.LineNum)
+ }
+ nodesByAlias[alias] = node
+ }
+
+ for _, child := range node.Children {
+ err := detectAliasDuplicates(child, nodesByAlias)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func prefixFromKmlNode(node *kml.Node, parent *Prefix) (*Prefix, error) {
+ cidr := Cidr(node.Key)
+ err := cidr.Validate()
+ if err != nil {
+ return nil, fmt.Errorf("invalid CIDR at line %d: %v", node.LineNum, err)
+ }
+
+ prefix := &Prefix{
+ Aliases: node.Aliases,
+ Cidr: cidr,
+ Labels: node.Attributes,
+ Parent: parent,
+ Children: make(map[string]*Prefix, len(node.Children)),
+ }
+
+ for _, childNode := range node.Children {
+ if prefix.Children[childNode.Key] != nil {
+ return nil, fmt.Errorf("duplicate prefix/CIDR: %s at line %d", childNode.Key, childNode.LineNum)
+ }
+
+ childPrefix, err := prefixFromKmlNode(childNode, prefix)
+ if err != nil {
+ return nil, err
+ }
+ prefix.Children[childNode.Key] = childPrefix
+ }
+
+ return prefix, nil
+}
diff --git a/klaabu/kml/marshal.go b/klaabu/kml/marshal.go
new file mode 100644
index 0000000..5ede95e
--- /dev/null
+++ b/klaabu/kml/marshal.go
@@ -0,0 +1,157 @@
+package kml
+
+import (
+ "github.com/erikkn/klaabu/klaabu/iputil"
+ "io"
+ "sort"
+ "strings"
+)
+
+type ColumnWidths struct {
+ key, aliases, attributes int
+}
+
+func (n *Node) Marshal(w io.Writer) error {
+ widths := &ColumnWidths{}
+ n.calculateColumnWidths(widths)
+ return n.marshal(w, widths)
+}
+
+func (n *Node) marshal(w io.Writer, widths *ColumnWidths) error {
+ if n.Key != "" {
+ err := n.marshalNode(w, widths)
+ if err != nil {
+ return err
+ }
+ }
+
+ sort.Slice(n.Children, func(i1, i2 int) bool {
+ min1, _, _ := iputil.MinMaxIP(n.Children[i1].Key)
+ min2, _, _ := iputil.MinMaxIP(n.Children[i2].Key)
+ cmp, _ := iputil.CompareIPs(min1, min2)
+ return cmp < 0
+ })
+
+ for index, child := range n.Children {
+ if child.Depth == 0 && index != 0 {
+ _, err := w.Write([]byte("\n"))
+ if err != nil {
+ return err
+ }
+ }
+
+ err := child.marshal(w, widths)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (n *Node) marshalNode(w io.Writer, widths *ColumnWidths) error {
+ // use builder and avoid intermediate strings to be GC/CPU efficient
+ b := strings.Builder{}
+ b.Grow(widths.key + widths.aliases + widths.attributes + len(n.Comment))
+
+ const singleIndent = " "
+ for i := 0; i < n.Depth; i++ {
+ b.WriteString(singleIndent)
+ }
+ b.WriteString(n.Key)
+ b.WriteString(": ")
+ for b.Len() < widths.key {
+ b.WriteString(" ")
+ }
+
+ const aliasSeparator = "|"
+ for i, alias := range n.Aliases {
+ if i != 0 {
+ b.WriteString(aliasSeparator)
+ }
+ b.WriteString(alias)
+ }
+ for b.Len() < widths.key+widths.aliases {
+ b.WriteString(" ")
+ }
+
+ if len(n.Attributes) > 0 {
+ b.WriteString("[")
+ attributesWritten := 0
+ const attrPairSeparator = ","
+ const attrKeyValueSeparator = "="
+
+ // make ordering consistent
+ attributeKeys := make([]string, 0, len(n.Attributes))
+ for k, _ := range n.Attributes {
+ attributeKeys = append(attributeKeys, k)
+ }
+ sort.Strings(attributeKeys)
+
+ for _, k := range attributeKeys {
+ v := n.Attributes[k]
+ if attributesWritten > 0 {
+ b.WriteString(attrPairSeparator)
+ }
+ b.WriteString(k)
+ b.WriteString(attrKeyValueSeparator)
+ b.WriteString(v)
+
+ attributesWritten++
+ }
+ b.WriteString("]")
+ }
+ for b.Len() < widths.key+widths.aliases+widths.attributes {
+ b.WriteString(" ")
+ }
+
+ if len(n.Comment) > 0 {
+ b.WriteString(" # ")
+ b.WriteString(n.Comment)
+ }
+
+ b.WriteString("\n")
+
+ _, err := w.Write([]byte(b.String()))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (n *Node) calculateColumnWidths(widths *ColumnWidths) {
+ nodeKeyWidth := n.Depth*2 + len(n.Key) + 2 // append ": "
+ if nodeKeyWidth > widths.key {
+ widths.key = nodeKeyWidth
+ }
+
+ nodeAliasesWidth := 0
+ for _, alias := range n.Aliases {
+ if nodeAliasesWidth > 0 {
+ nodeAliasesWidth += 1 // separator
+ }
+ nodeAliasesWidth += len(alias)
+ }
+ if nodeAliasesWidth > 0 {
+ nodeAliasesWidth += 1 // padding
+ }
+ if nodeAliasesWidth > widths.aliases {
+ widths.aliases = nodeAliasesWidth
+ }
+
+ nodeAttrsWidth := 2
+ for k, v := range n.Attributes {
+ if nodeAttrsWidth > 2 {
+ nodeAttrsWidth += 1 // separator
+ }
+ nodeAttrsWidth += len(k) + 1 + len(v)
+ }
+ if nodeAttrsWidth > widths.attributes {
+ widths.attributes = nodeAttrsWidth
+ }
+
+ for _, child := range n.Children {
+ child.calculateColumnWidths(widths)
+ }
+}
diff --git a/klaabu/kml/parser.go b/klaabu/kml/parser.go
new file mode 100644
index 0000000..30452c7
--- /dev/null
+++ b/klaabu/kml/parser.go
@@ -0,0 +1,146 @@
+package kml
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+)
+
+var (
+ ignorableLineRx = regexp.MustCompile(`^\s*(?:\#.*)?$`)
+ nodeLineRx = regexp.MustCompile(`^(?P\s*)(?P\S+):\s*(?P[^\[#\s]+)?\s*(?:\[(?P.*)\])?\s*\#?\s*(?P.*)?$`)
+ sub map[string]int
+)
+
+func init() {
+ names := nodeLineRx.SubexpNames()
+ sub = make(map[string]int, len(names)-1)
+ for i, name := range names {
+ sub[name] = i
+ }
+}
+
+type Node struct {
+ Aliases []string
+ Attributes map[string]string
+ Children []*Node
+ Comment string
+ Depth int
+ Key string
+ LineNum int
+ Parent *Node
+}
+
+func newNode() *Node {
+ return &Node{
+ Aliases: make([]string, 0, 1),
+ Attributes: make(map[string]string),
+ Children: make([]*Node, 0, 4),
+ }
+}
+
+func Parse(reader io.Reader) (*Node, error) {
+ root := newNode()
+ root.Depth = -1
+
+ parent := root
+
+ s := bufio.NewScanner(reader)
+ lineNum := 0
+
+ commonIndentChar := '?'
+ commonSingleIndentSize := -1
+
+ for s.Scan() {
+ lineNum++
+
+ line := s.Text()
+ if ignorableLineRx.MatchString(line) {
+ continue
+ }
+
+ node := newNode()
+
+ groups := nodeLineRx.FindStringSubmatch(line)
+ if groups == nil {
+ return nil, fmt.Errorf("invalid kml: line %d: '%s'", lineNum, line)
+ }
+
+ // indentation
+ indent := groups[sub["indent"]]
+ indentSize := 0
+ indentChar := '?'
+ for _, c := range indent {
+ indentSize++
+ if indentChar == '?' {
+ indentChar = c
+ } else if indentChar != c {
+ return nil, fmt.Errorf("invalid kml: line %d: inconsistent indent character '%c' %U %q", lineNum, c, c, c)
+ }
+ }
+ if indentChar != '?' {
+ if commonIndentChar == '?' {
+ commonIndentChar = indentChar
+ } else if commonIndentChar != indentChar {
+ return nil, fmt.Errorf("invalid kml: line %d: inconsistent indent character '%c' %U %q", lineNum, indentChar, indentChar, indentChar)
+ }
+ if commonSingleIndentSize == -1 {
+ commonSingleIndentSize = indentSize
+ }
+ }
+ if indentSize%commonSingleIndentSize != 0 {
+ return nil, fmt.Errorf("invalid kml: line %d: inconsistent indentation size, %d is not multiple of %d", lineNum, indentSize, commonSingleIndentSize)
+ }
+ node.Depth = indentSize / commonSingleIndentSize
+ for node.Depth <= parent.Depth {
+ // not checking for parent.Parent == nil as it should not be possible
+ parent = parent.Parent
+ }
+ if node.Depth == parent.Depth+2 {
+ if len(parent.Children) == 0 {
+ return nil, fmt.Errorf("invalid kml: line %d: invalid indent level", lineNum)
+ }
+ parent = parent.Children[len(parent.Children)-1]
+ }
+ if node.Depth != parent.Depth+1 {
+ return nil, fmt.Errorf("invalid kml: line %d: invalid indent level", lineNum)
+ }
+
+ for _, v := range strings.Split(groups[sub["aliases"]], "|") {
+ if v != "" {
+ node.Aliases = append(node.Aliases, v)
+ }
+ }
+
+ attrKvStrings := strings.Split(groups[sub["attrs"]], ",")
+ node.Attributes = make(map[string]string, len(attrKvStrings))
+ for _, kvString := range attrKvStrings {
+ kv := strings.Split(kvString, "=")
+ for i, v := range kv {
+ kv[i] = strings.TrimSpace(v)
+ }
+
+ if len(kv) == 2 {
+ node.Attributes[kv[0]] = kv[1]
+ } else if len(kv) == 1 && kv[0] != "" {
+ node.Attributes[kv[0]] = "true"
+ }
+ }
+
+ node.Comment = strings.TrimSpace(groups[sub["comment"]])
+
+ node.Key = groups[sub["key"]]
+ node.LineNum = lineNum
+
+ node.Parent = parent
+ parent.Children = append(parent.Children, node)
+ }
+
+ if err := s.Err(); err != nil && err != io.EOF {
+ return nil, err
+ }
+
+ return root, nil
+}
diff --git a/klaabu/kml/parser_test.go b/klaabu/kml/parser_test.go
new file mode 100644
index 0000000..a1c1562
--- /dev/null
+++ b/klaabu/kml/parser_test.go
@@ -0,0 +1,192 @@
+package kml
+
+import (
+ "strings"
+ "testing"
+)
+
+// TODO negative test cases
+
+func mustParse(t *testing.T, s string) (root *Node, children []*Node) {
+ root, err := Parse(strings.NewReader(s))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if root == nil {
+ t.Fatal("root is nil")
+ }
+
+ return root, root.Children
+}
+
+func Test_EmptyAndBasics(t *testing.T) {
+ root, nodes := mustParse(t, `# comments and the blank lines are ignored
+
+ # comments can be indented
+
+empty:`)
+
+ if len(nodes) != 1 {
+ t.Fatalf("invalid number of nodes")
+ }
+
+ empty := nodes[0]
+ if empty.Key != "empty" {
+ t.Fatalf("expected empty, got %s", empty.Key)
+ }
+ if len(empty.Children) != 0 {
+ t.Fatal("empty got children")
+ }
+ if len(empty.Attributes) != 0 {
+ t.Fatal("empty got attributes")
+ }
+ if empty.Comment != "" {
+ t.Fatal("empty got comment")
+ }
+ if len(empty.Aliases) != 0 {
+ t.Fatalf("empty got aliases")
+ }
+ if empty.Depth != 0 {
+ t.Fatal("empty depth != 0")
+ }
+ if empty.Parent != root {
+ t.Fatal("empty parent not root")
+ }
+ if empty.LineNum != 5 {
+ t.Fatal("empty line number not 6")
+ }
+}
+
+func assertAlias(t *testing.T, node *Node, expected ...string) {
+ if len(expected) != len(node.Aliases) {
+ t.Fatalf("invalid %s aliases: expected %v, actual %v", node.Key, expected, node.Aliases)
+ }
+
+ for index, expectedValue := range expected {
+ actualValue := node.Aliases[index]
+ if actualValue != expectedValue {
+ t.Fatalf("invalid %s aliases: expected %v, actual %v", node.Key, expected, node.Aliases)
+ }
+ }
+}
+
+func Test_Alias(t *testing.T) {
+ _, nodes := mustParse(t, `
+alias-1: alias
+alias-multi-trim: alias|blias|clias
+`)
+
+ assertAlias(t, nodes[0], "alias")
+ assertAlias(t, nodes[1], "alias", "blias", "clias")
+}
+
+func assertAttr(t *testing.T, node *Node, expectedList ...string) {
+ actual := node.Attributes
+
+ expected := make(map[string]string, len(expectedList)/2)
+ for i := 0; i < len(expectedList); i += 2 {
+ expected[expectedList[i]] = expectedList[i+1]
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("invalid %s attributes: expected %v, got %v", node.Key, expected, actual)
+ }
+
+ for k, expectedValue := range expected {
+ actualValue, actualKnown := actual[k]
+ if !actualKnown {
+ t.Fatalf("invalid %s attributes: expected %v, got %v", node.Key, expected, actual)
+ }
+ if actualValue != expectedValue {
+ t.Fatalf("invalid %s attributes: expected %v, got %v", node.Key, expected, actual)
+ }
+ }
+}
+
+func Test_Attr(t *testing.T) {
+ _, nodes := mustParse(t, `
+attr-empty: []
+attr-1: [key=value]
+attr-multi-trim: [ k1 = v1 , k2 = v2 , k3 = v3 ]
+attr-bool: [disabled]
+`)
+ if len(nodes) != 4 {
+ t.Fatal("invalid number of nodes")
+ }
+
+ assertAttr(t, nodes[0])
+ assertAttr(t, nodes[1], "key", "value")
+ assertAttr(t, nodes[2], "k1", "v1", "k2", "v2", "k3", "v3")
+ assertAttr(t, nodes[3], "disabled", "true")
+}
+
+func assertComment(t *testing.T, node *Node, expected string) {
+ if node.Comment != expected {
+ t.Fatalf("invalid %s comment: expected '%s', actual '%s'", node.Key, expected, node.Comment)
+ }
+}
+
+func Test_Comment(t *testing.T) {
+ _, nodes := mustParse(t, `
+comment: #todo
+comment-trim: # reserved for future use
+`)
+
+ assertComment(t, nodes[0], "todo")
+ assertComment(t, nodes[1], "reserved for future use")
+}
+
+func Test_Combos(t *testing.T) {
+ _, nodes := mustParse(t, `
+# indentation for better readability
+alias-attr: mgmt [deprecated]
+alias-comment: mgmt #todo remove
+alias-attr-comment: mgmt [deprecated] #todo remove
+attr-comment: [deprecated] #todo remove
+`)
+
+ assertAlias(t, nodes[0], "mgmt")
+ assertAttr(t, nodes[0], "deprecated", "true")
+ assertComment(t, nodes[0], "")
+
+ assertAlias(t, nodes[1], "mgmt")
+ assertAttr(t, nodes[1])
+ assertComment(t, nodes[1], "todo remove")
+
+ assertAlias(t, nodes[2], "mgmt")
+ assertAttr(t, nodes[2], "deprecated", "true")
+ assertComment(t, nodes[2], "todo remove")
+
+ assertAlias(t, nodes[3])
+ assertAttr(t, nodes[3], "deprecated", "true")
+ assertComment(t, nodes[3], "todo remove")
+}
+
+func Test_Indent(t *testing.T) {
+ _, nodes := mustParse(t, `
+root:
+ child1:
+ grandchild1:
+ child2:
+# sneaky comment
+ grandchild2:
+un-indent-2:
+`)
+
+ if len(nodes) != 2 {
+ t.Fatal("invalid number of nodes")
+ }
+
+ root := nodes[0]
+ if len(root.Children) != 2 {
+ t.Fatal("invalid number of root children")
+ }
+ child1 := root.Children[0]
+ if len(child1.Children) != 1 && child1.Children[0].Key != "grandchild1" {
+ t.Fatal("invalid grandchild1")
+ }
+ child2 := root.Children[1]
+ if len(child2.Children) != 1 && child2.Children[0].Key != "grandchild2" {
+ t.Fatal("invalid grandchild2")
+ }
+}
diff --git a/klaabu/prefix.go b/klaabu/prefix.go
new file mode 100644
index 0000000..a40e2df
--- /dev/null
+++ b/klaabu/prefix.go
@@ -0,0 +1,156 @@
+package klaabu
+
+import (
+ "fmt"
+ "github.com/erikkn/klaabu/klaabu/iputil"
+ "sort"
+)
+
+// Cidr represents a single CIDR notation.
+type Cidr string
+
+// Prefix is data for the schema.
+type Prefix struct {
+ Aliases []string
+ Cidr Cidr
+ Labels map[string]string
+ Parent *Prefix
+ Children map[string]*Prefix
+}
+
+type LabelSearchTerm struct {
+ Key string
+ Value *string
+}
+
+func (p *Prefix) match(term *LabelSearchTerm) bool {
+ for k, v := range p.Labels {
+ if k == term.Key && (term.Value == nil || v == *term.Value) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// FindPrefixesByLabelTerms fetches every Prefix that has the single 'key' and 'value' label pair set by traversing down the tree starting from whatever value `c` is.
+func (p *Prefix) FindPrefixesByLabelTerms(terms []*LabelSearchTerm) []*Prefix {
+ parent := p
+ result := make([]*Prefix, 0)
+
+ parentMatches := true
+ for _, term := range terms {
+ if !parent.match(term) {
+ parentMatches = false
+ }
+ }
+ if parentMatches {
+ result = append(result, parent)
+ }
+
+ for _, child := range parent.Children {
+ // Note that we now call this method on the actual child of the parent; tree recursion.
+ result = append(result, child.FindPrefixesByLabelTerms(terms)...)
+ }
+
+ return result
+}
+
+func (p *Prefix) PrefixById(id string) *Prefix {
+ if p.Cidr == Cidr(id) {
+ return p
+ }
+
+ for _, alias := range p.Aliases {
+ if alias == id {
+ return p
+ }
+ }
+
+ for _, child := range p.Children {
+ prefixFromChild := child.PrefixById(id)
+ if prefixFromChild != nil {
+ return prefixFromChild
+ }
+ }
+
+ return nil
+}
+
+// FindPrefixesByLabelNamesValues fetches every Prefix that has all the 'key' and 'value' label pairs set that are passed through 'l'. Every item of slice 'l' contains a key&value pair following the 'key=value' notation. This Function starts traversing down the tree from whatever value 'c' is.
+//func (c *Prefix) FindPrefixesByLabelNamesValues(l []string) []*Prefix {
+// labels :=
+//
+// return nil
+//}
+
+// AvailableIpSpace returns the available IP space within `c`. C doesn't have to be the parent, if c is a child, this func will return the available IP space in that child.
+func (p *Prefix) AvailableIpSpace() ([]Cidr, error) {
+ // TODO unfinished
+ usedSpace := make([]Cidr, 0, len(p.Children))
+ //availableCidrs := []Cidr{}
+
+ for _, child := range p.Children {
+ usedSpace = append(usedSpace, child.Cidr)
+ }
+
+ sort.Slice(usedSpace, func(i, j int) bool {
+ c1 := usedSpace[i]
+ c2 := usedSpace[j]
+
+ c1Min, _, err := c1.MinMaxIP()
+ if err != nil {
+ return false
+ }
+
+ c2Min, _, err := c2.MinMaxIP()
+ if err != nil {
+ return false
+ }
+
+ compareCidrs, err := iputil.CompareIPs(c1Min, c2Min)
+ if err != nil {
+ return false
+ }
+
+ return compareCidrs < 0
+ })
+
+ minPrefix, maxPrefix, err := p.Cidr.MinMaxIP()
+ if err != nil {
+ return nil, fmt.Errorf("%s", err)
+ }
+ minAvailable := minPrefix
+
+ for _, cidr := range usedSpace {
+ minUsed, maxUsed, err := cidr.MinMaxIP()
+ if err != nil {
+ return nil, err
+ }
+
+ minAvailableCmpMinUsed, err := iputil.CompareIPs(minAvailable, minUsed)
+ if err != nil {
+ return nil, err
+ }
+
+ if minAvailableCmpMinUsed < 0 {
+ fmt.Printf("available: %v - %v \n", minAvailable, minUsed)
+ //availableCidrs := append(availableCidrs, cidr)
+ }
+
+ // Plus 1 (+1), next available IP
+ minAvailable = maxUsed
+ }
+
+ minAvailableCmpMaxPrefix, err := iputil.CompareIPs(minAvailable, maxPrefix)
+ if err != nil {
+ return nil, err
+ }
+
+ if minAvailableCmpMaxPrefix < 0 {
+ fmt.Printf("available: %v - %v \n", minAvailable, maxPrefix)
+ //availableCidrs := append(availableCidrs, cidr)
+ }
+
+ return nil, nil
+}
diff --git a/klaabu/schema.go b/klaabu/schema.go
new file mode 100644
index 0000000..40d7247
--- /dev/null
+++ b/klaabu/schema.go
@@ -0,0 +1,52 @@
+package klaabu
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+)
+
+// Schema is the Klaabu schema.
+type Schema struct {
+ Version string
+ Labels map[string]string
+ Root *Prefix
+}
+
+// NewSchema creates and returns a new Schema.
+func NewSchema(labels map[string]string) *Schema {
+ return &Schema{
+ Version: "v1",
+ Labels: labels,
+ }
+}
+
+func (s *Schema) PrefixById(id string) *Prefix {
+ return s.Root.PrefixById(id)
+}
+
+// Validate checks if you are stupid or not.
+func (s *Schema) Validate() error {
+ err := s.Root.Validate()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// WriteSchemaToFile takes a schema and writes it to a file on disk.
+func WriteSchemaToFile(s *Schema, fileName *string) error {
+ // TODO use kml
+ data, err := []byte{}, errors.New("not implemented")
+ if err != nil {
+ return fmt.Errorf("error in serialization: %s", err)
+ }
+
+ err = ioutil.WriteFile(*fileName, data, 0644)
+ if err != nil {
+ return fmt.Errorf("error writing schema to file: %s", err)
+ }
+
+ return nil
+}
diff --git a/klaabu/terraform/README.md b/klaabu/terraform/README.md
new file mode 100644
index 0000000..3862173
--- /dev/null
+++ b/klaabu/terraform/README.md
@@ -0,0 +1,3 @@
+# Terraform package
+
+ermommmmm`u
diff --git a/klaabu/terraform/terraform.go b/klaabu/terraform/terraform.go
new file mode 100644
index 0000000..801ecce
--- /dev/null
+++ b/klaabu/terraform/terraform.go
@@ -0,0 +1,49 @@
+package terraform
+
+import (
+ "encoding/json"
+
+ "github.com/erikkn/klaabu/klaabu"
+)
+
+type module struct {
+ Output map[string]output `json:"output"`
+}
+
+type output struct {
+ Value map[string]prefix `json:"value"`
+}
+
+type prefix struct {
+ Cidr string `json:"cidr"`
+ Labels map[string]string `json:"labels,omitempty"`
+}
+
+// Generate takes the schema as input (s) and generates JSON data which can be used by Terraform.
+func Generate(s *klaabu.Schema) ([]byte, error) {
+ aliases := make(map[string]prefix)
+
+ m := module{
+ Output: map[string]output{
+ "aliases": output{
+ Value: aliases,
+ },
+ },
+ }
+
+ populatePrefixes(s.Root, aliases)
+ return json.MarshalIndent(m, "", " ")
+}
+
+func populatePrefixes(p *klaabu.Prefix, aliases map[string]prefix) {
+ // We don't want to visualize root (transient field).
+ if p.Cidr != klaabu.Cidr("0.0.0.0/0") {
+ for _, alias := range p.Aliases {
+ aliases[alias] = prefix{Cidr: string(p.Cidr), Labels: p.Labels}
+ }
+ }
+
+ for _, child := range p.Children {
+ populatePrefixes(child, aliases)
+ }
+}
diff --git a/klaabu/validate.go b/klaabu/validate.go
new file mode 100644
index 0000000..f2f8d3a
--- /dev/null
+++ b/klaabu/validate.go
@@ -0,0 +1,142 @@
+package klaabu
+
+import (
+ "fmt"
+ "github.com/erikkn/klaabu/klaabu/iputil"
+ "net"
+)
+
+// MinMaxIP lalala.
+func (c *Cidr) MinMaxIP() (net.IP, net.IP, error) {
+ _, ipNet, err := net.ParseCIDR(string(*c))
+ if err != nil {
+ return nil, nil, fmt.Errorf("error while parsing your CIDR %v with error: %s", string(*c), err)
+ }
+
+ min := make([]byte, len(ipNet.IP))
+ max := make([]byte, len(ipNet.IP))
+ for i := range ipNet.IP {
+ min[i] = ipNet.Mask[i] & ipNet.IP[i]
+ max[i] = ipNet.Mask[i]&ipNet.IP[i] | ^ipNet.Mask[i]
+ }
+
+ return min, max, nil
+}
+
+// IsChildOf llalala
+func (c *Cidr) IsChildOf(parent Cidr) (bool, error) {
+ minChild, maxChild, err := c.MinMaxIP()
+ if err != nil {
+ return false, fmt.Errorf("error: %s", err)
+ }
+
+ minParent, maxParent, err := parent.MinMaxIP()
+ if err != nil {
+ return false, fmt.Errorf("error: %s", err)
+ }
+
+ minComparison, err := iputil.CompareIPs(minParent, minChild)
+ if err != nil {
+ return false, err
+ }
+ maxComparison, err := iputil.CompareIPs(maxChild, maxParent)
+ if err != nil {
+ return false, err
+ }
+
+ return minComparison <= 0 && maxComparison <= 0, nil
+}
+
+// OverlapsCidr checks if `c` and `cidr` are overlapping or not and returns a boolean.
+func (c *Cidr) OverlapsCidr(cidr Cidr) (bool, error) {
+
+ minA, maxA, err := c.MinMaxIP()
+ if err != nil {
+ return false, fmt.Errorf("error while calculating the min & max IPs of %s with error message: %s", *c, err)
+ }
+
+ minB, maxB, err := cidr.MinMaxIP()
+ if err != nil {
+ return false, fmt.Errorf("error while calculating the min & max IPs of %s with error message: %s", cidr, err)
+
+ }
+
+ maxACmpMinB, err := iputil.CompareIPs(maxA, minB)
+ if err != nil {
+ return false, fmt.Errorf("unable to compare %s and %s with error message: %s", maxA, minB, err)
+ }
+
+ minACmpMaxB, err := iputil.CompareIPs(minA, maxB)
+ if err != nil {
+ return false, fmt.Errorf("unable to compare %s and %s with error message: %s", minA, maxB, err)
+ }
+
+ return maxACmpMinB >= 0 && minACmpMaxB <= 0, nil
+}
+
+// ValidateChildrenOverlap checks if there are any overlaps between immediate children of a prefix.
+func (p *Prefix) ValidateChildrenOverlap() error {
+ children := make([]*Prefix, 0, len(p.Children))
+
+ for _, v := range p.Children {
+ children = append(children, v)
+ }
+
+ for x := 0; x < len(children); x++ {
+ for y := x + 1; y < len(children); y++ {
+ overlap, err := children[x].Cidr.OverlapsCidr(children[y].Cidr)
+ if err != nil {
+ return fmt.Errorf("unable to call `OverlapsCidr` with error message: %s", err)
+ }
+
+ if overlap {
+ return fmt.Errorf("%s is overlapping with %s", children[x].Cidr, children[y].Cidr)
+ }
+ }
+ }
+
+ return nil
+}
+
+// Validate validates a CIDR.
+func (c *Cidr) Validate() error {
+ _, _, err := net.ParseCIDR(string(*c))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Validate checks if you are stupid or not.
+func (p *Prefix) Validate() error {
+ // Checks the vadility of the actual CIDR of the Prefix
+ err := p.Cidr.Validate()
+ if err != nil {
+ return fmt.Errorf("invalid CIDR '%s': %s", p.Cidr, err)
+ }
+
+ // Checks if the children has any overlapping CIDR.
+ err = p.ValidateChildrenOverlap()
+ if err != nil {
+ return fmt.Errorf("error: %s", err)
+ }
+
+ for _, v := range p.Children {
+ isChild, err := v.Cidr.IsChildOf(p.Cidr)
+ if err != nil {
+ return err
+ }
+
+ if !isChild {
+ return fmt.Errorf("%v is not a valid child CIDR of %v", v.Cidr, p.Cidr)
+ }
+
+ err = v.Validate()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/klaabu/validate_test.go b/klaabu/validate_test.go
new file mode 100644
index 0000000..eb1ab17
--- /dev/null
+++ b/klaabu/validate_test.go
@@ -0,0 +1,52 @@
+package klaabu
+
+import (
+ "net"
+ "reflect"
+ "testing"
+)
+
+func TestCidr_MinMaxIP(t *testing.T) {
+ data := []string{
+ "10.0.0.0/8", "10.0.0.0", "10.255.255.255",
+ "10.0.0.0/30", "10.0.0.0", "10.0.0.3",
+ "10.0.0.0/32", "10.0.0.0", "10.0.0.0",
+ "10.1.1.1/32", "10.1.1.1", "10.1.1.1",
+ "172.24.32.0/20", "172.24.32.0", "172.24.47.255",
+ "0.0.0.0/0", "0.0.0.0", "255.255.255.255",
+ }
+
+ for i := 0; i < len(data); i += 3 {
+ cidr := Cidr(data[i])
+ expectedMin := net.ParseIP(data[i+1])
+ expectedMax := net.ParseIP(data[i+2])
+
+ min, max, err := cidr.MinMaxIP()
+ if err != nil {
+ t.Errorf("error: %s", err)
+ }
+
+ if !min.Equal(expectedMin) {
+ t.Errorf("Expected: %v, got: %v", expectedMin, min)
+ }
+
+ if !max.Equal(expectedMax) {
+ t.Errorf("Expected: %v, got: %v", expectedMax, max)
+ }
+ }
+}
+
+func Test_ParseCidr(t *testing.T) {
+ _, ipNet, err := net.ParseCIDR("10.0.0.0/8")
+ if err != nil {
+ t.Errorf("error: %s", err)
+ }
+
+ if !reflect.DeepEqual([]byte(ipNet.IP), []byte{10, 0, 0, 0}) {
+ t.Errorf("Expected: 10.0.0.0 got: %v", ipNet.IP)
+ }
+
+ if !reflect.DeepEqual([]byte(ipNet.Mask), []byte{255, 0, 0, 0}) {
+ t.Errorf("Expected: 10.255.255.255, got: %v", ipNet.Mask)
+ }
+}