-
-
Save mediocregopher/5fa34526054a85a15292a861a1529074 to your computer and use it in GitHub Desktop.
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
package main | |
// This binary accepts the output of `garage layout show` into its stdin, and | |
// it outputs a newline-delimited set of `garage layout $cmd` strings on | |
// stdout. The layout commands which are output will, if run, bring the current | |
// node's layout on the cluster up-to-date with what's in daemon.yml. | |
import ( | |
"bufio" | |
"bytes" | |
"errors" | |
"fmt" | |
"io" | |
"os" | |
"strconv" | |
"strings" | |
"cryptic-net/garage/garageutils" | |
"cryptic-net/globals" | |
) | |
type clusterNode struct { | |
ID string | |
Zone string | |
Capacity int | |
} | |
type clusterNodes []clusterNode | |
func (n clusterNodes) get(id string) (clusterNode, bool) { | |
var ok bool | |
for _, node := range n { | |
if len(node.ID) > len(id) { | |
ok = strings.HasPrefix(node.ID, id) | |
} else { | |
ok = strings.HasPrefix(id, node.ID) | |
} | |
if ok { | |
return node, true | |
} | |
} | |
return clusterNode{}, false | |
} | |
var currClusterLayoutVersionB = []byte("Current cluster layout version:") | |
func readCurrNodes(r io.Reader) (clusterNodes, int, error) { | |
input, err := io.ReadAll(r) | |
if err != nil { | |
return nil, 0, fmt.Errorf("reading stdin: %w", err) | |
} | |
// NOTE I'm not sure if this check should be turned on or not. It simplifies | |
// things to turn it off and just say that no one should ever be manually | |
// messing with the layout, but on the other hand maybe someone might? | |
// | |
//if i := bytes.Index(input, []byte("==== STAGED ROLE CHANGES ====")); i >= 0 { | |
// return nil, 0, errors.New("cluster layout has staged changes already, won't modify") | |
//} | |
/* The first section of input will always be something like this: | |
``` | |
==== CURRENT CLUSTER LAYOUT ==== | |
ID Tags Zone Capacity | |
AAA… ZZZ 1 | |
BBB… ZZZ 1 | |
CCC… ZZZ 1 | |
Current cluster layout version: N | |
``` | |
There may be more, depending on if the cluster already has changes staged, | |
but this will definitely be first. */ | |
i := bytes.Index(input, currClusterLayoutVersionB) | |
if i < 0 { | |
return nil, 0, errors.New("no current cluster layout found in input") | |
} | |
input, tail := input[:i], input[i:] | |
var currNodes clusterNodes | |
for inputBuf := bufio.NewReader(bytes.NewBuffer(input)); ; { | |
line, err := inputBuf.ReadString('\n') | |
if errors.Is(err, io.EOF) { | |
break | |
} else if err != nil { | |
return nil, 0, fmt.Errorf("reading input line by line from buffer: %w", err) | |
} | |
fields := strings.Fields(line) | |
if len(fields) < 3 { | |
continue | |
} | |
id := fields[0] | |
// The ID will always be given ending in this fucked up ellipses | |
if trimmedID := strings.TrimSuffix(id, "…"); id == trimmedID { | |
continue | |
} else { | |
id = trimmedID | |
} | |
zone := fields[1] | |
capacity, err := strconv.Atoi(fields[2]) | |
if err != nil { | |
return nil, 0, fmt.Errorf("parsing capacity %q: %w", fields[2], err) | |
} | |
currNodes = append(currNodes, clusterNode{ | |
ID: id, | |
Zone: zone, | |
Capacity: capacity, | |
}) | |
} | |
// parse current cluster version from tail | |
tail = bytes.TrimPrefix(tail, currClusterLayoutVersionB) | |
if i := bytes.Index(tail, []byte("\n")); i > 0 { | |
tail = tail[:i] | |
} | |
tail = bytes.TrimSpace(tail) | |
version, err := strconv.Atoi(string(tail)) | |
if err != nil { | |
return nil, 0, fmt.Errorf("parsing version string from %q: %w", tail, err) | |
} | |
return currNodes, version, nil | |
} | |
func readExpNodes(env *globals.Env) (clusterNodes, error) { | |
var expNodes clusterNodes | |
for _, alloc := range env.ThisDaemon().Storage.Allocations { | |
id, err := garageutils.GeneratePeerID(env.ThisHost.IP, alloc.RPCPort) | |
if err != nil { | |
return nil, fmt.Errorf( | |
"generating peer id for ip:%q port:%d: %w", | |
env.ThisHost.IP, alloc.RPCPort, err, | |
) | |
} | |
expNodes = append(expNodes, clusterNode{ | |
ID: id, | |
Zone: env.ThisHost.Name, | |
Capacity: alloc.Capacity / 100, | |
}) | |
} | |
return expNodes, nil | |
} | |
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has | |
// fully expanded ids, currNodes are abbreviated. | |
func diff(currNodes, expNodes clusterNodes) []string { | |
var lines []string | |
for _, node := range currNodes { | |
if _, ok := expNodes.get(node.ID); !ok { | |
lines = append( | |
lines, | |
fmt.Sprintf("garage layout remove %s", node.ID), | |
) | |
} | |
} | |
for _, expNode := range expNodes { | |
currNode, ok := currNodes.get(expNode.ID) | |
currNode.ID = expNode.ID // so that equality checking works | |
if ok && currNode == expNode { | |
continue | |
} | |
lines = append( | |
lines, | |
fmt.Sprintf( | |
"garage layout assign %s -z %s -c %d", | |
expNode.ID, | |
expNode.Zone, | |
expNode.Capacity, | |
), | |
) | |
} | |
return lines | |
} | |
func main() { | |
env, err := globals.ReadEnv() | |
if err != nil { | |
panic(fmt.Errorf("reading environment: %w", err)) | |
} | |
currNodes, currVersion, err := readCurrNodes(os.Stdin) | |
if err != nil { | |
panic(fmt.Errorf("reading current layout from stdin: %w", err)) | |
} | |
thisCurrNodes := make(clusterNodes, 0, len(currNodes)) | |
for _, node := range currNodes { | |
if env.ThisHost.Name != node.Zone { | |
continue | |
} | |
thisCurrNodes = append(thisCurrNodes, node) | |
} | |
expNodes, err := readExpNodes(env) | |
if err != nil { | |
panic(fmt.Errorf("reading expected layout from environment: %w", err)) | |
} | |
lines := diff(thisCurrNodes, expNodes) | |
if len(lines) == 0 { | |
return | |
} | |
for _, line := range lines { | |
fmt.Println(line) | |
} | |
fmt.Printf("garage layout apply --version %d\n", currVersion+1) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment