~egtann/inv2config

3230677e5cadab2013c9feebf0002a0c58535c46 — Evan Tann 9 months ago
initial commit
6 files changed, 199 insertions(+), 0 deletions(-)

A LICENSE
A README.md
A go.mod
A go.sum
A main.go
A srp.go
A  => LICENSE +13 -0
@@ 1,13 @@
Copyright 2020 Evan Tann

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

A  => README.md +62 -0
@@ 1,62 @@
# inventory2config

Takes an inventory.json and a service-based SRP config file as inputs and
outputs an SRP config file pointing at specific IPs.

Example inventory.json:

```json
{"10.128.0.1":["my-app"]}
```

Example service-based SRP config file:

```json
{
	"Services": {
		"www.example.com": {
			"HealthPath": "/health",
			"Service": "my-app",
			"Port": 3000
		},
		"www-example.internal": {
			"HealthPath": "/health",
			"Service": "my-app",
			"Port": 3001
		}
	}
}
```

To use:

```bash
inventory2config -i inventory.json -s config.json > config.json
```

This outputs a config.json with backends populated with the correct ip:port
combinations like so:

```json
{
	"Services": {
		"www.example.com": {
			"HealthPath": "/health",
			"Service": "my-app",
			"Port": 3000,
			"Backends": ["10.128.0.1:3000"]
		},
		"www-example.internal": {
			"HealthPath": "/health",
			"Service": "my-app",
			"Port": 3001,
			"Backends": ["10.128.0.1:3001"]
		}
	}
}
```

SRP will ignore the Service and Port. They are added for this tool only.

In the event of an error, inventory2json will still output the original file to
stdout, so no data is lost.

A  => go.mod +5 -0
@@ 1,5 @@
module git.sr.ht/~egtann/inventory2config

go 1.14

require git.sr.ht/~egtann/up v0.0.0-20200324172709-10f0fb584d43

A  => go.sum +2 -0
@@ 1,2 @@
git.sr.ht/~egtann/up v0.0.0-20200324172709-10f0fb584d43 h1:0dsb/Oivfxhd0HDxbIx5SzefqcaYeqL+TWihyNMaLsk=
git.sr.ht/~egtann/up v0.0.0-20200324172709-10f0fb584d43/go.mod h1:z51L8eNGLPT0f3d/+8danxnrOmuXRh4Ca9HcviG2D0M=

A  => main.go +97 -0
@@ 1,97 @@
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"os"

	"git.sr.ht/~egtann/up"
)

func main() {
	// Always send to stdout our new SRP or the original version
	srpOut, err := run()
	fmt.Printf(srpOut)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() (string, error) {
	// Read an inventory file and an SRP config file
	var (
		invFile = flag.String("i", "inventory.json", "inventory file")
		srpFile = flag.String("s", "config.json", "srp config file")
	)
	flag.Parse()

	srpByt, err := ioutil.ReadFile(*srpFile)
	if err != nil {
		return "", fmt.Errorf("read srp config: %w", err)
	}
	origSRP := string(srpByt)
	conf := srpConfig{}
	if err = json.Unmarshal(srpByt, &conf); err != nil {
		return origSRP, fmt.Errorf("decode srp config: %w", err)
	}

	inventory, err := parseInventory(*invFile)
	if err != nil {
		return origSRP, fmt.Errorf("parse inventory: %w", err)
	}

	// Gee willikers, Batman, this is O(N^3)
	for ip, serviceNames := range inventory {
	middle:
		for _, name := range serviceNames {
			for _, backend := range conf.Services {
				if backend.Service != name {
					continue
				}
				if ip == "" {
					continue
				}
				ipPort := fmt.Sprintf("%s:%d", ip, backend.Port)
				if backend.Port == 0 {
					ipPort = ip
				}
				backend.Backends = append(backend.Backends, ipPort)
				conf.Services[name] = backend
				break middle
			}
		}
	}

	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetIndent("", "\t")
	if err := enc.Encode(conf); err != nil {
		return origSRP, fmt.Errorf("encode config: %w", err)
	}

	// TODO(egtann) output the original config via stdout if any error
	// happens.
	//
	// TODO(egtann) on success, output the config formatted for human
	// editing (i.e. prettify)

	return string(buf.Bytes()), nil
}

func parseInventory(filename string) (up.Inventory, error) {
	fi, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer func() { _ = fi.Close() }()

	inv := up.Inventory{}
	if err = json.NewDecoder(fi).Decode(&inv); err != nil {
		return nil, fmt.Errorf("decode: %w", err)
	}
	return inv, nil
}

A  => srp.go +20 -0
@@ 1,20 @@
package main

type srpConfig struct {
	Services map[string]*backend
	API      struct{ Subnet string }
}

type backend struct {
	HealthPath string    `json:",omitempty"`
	Backends   []string  `json:",omitempty"`
	Redirect   *redirect `json:",omitempty"`
	Service    string    `json:",omitempty"`
	Port       int       `json:",omitempty"`
}

type redirect struct {
	URL         string `json:",omitempty"`
	Permanent   bool   `json:",omitempty"`
	DiscardPath bool   `json:",omitempty"`
}