Compare commits

..

3 Commits

Author SHA1 Message Date
Alejandro Mery 4ab2c9d068 reflective [WIP] 10 months ago
Alejandro Mery 098482a890 wireguard: ini.Unmarshal() [WIP] 10 months ago
Alejandro Mery 0a48a40796 build-sys: use local asciigoat.org/ini 10 months ago
  1. 1
      .gitignore
  2. 29
      Makefile
  3. 2
      cmd/jpictl/dns.go
  4. 9
      cmd/jpictl/gateway.go
  5. 200
      cmd/jpictl/list.go
  6. 7
      cmd/jpictl/main.go
  7. 31
      cmd/jpictl/version.go
  8. 53
      go.mod
  9. 95
      go.sum
  10. 3
      pkg/ceph/config.go
  11. 59
      pkg/ceph/config_parser.go
  12. 43
      pkg/cluster/addr.go
  13. 2
      pkg/cluster/ceph.go
  14. 9
      pkg/cluster/ceph_scan.go
  15. 8
      pkg/cluster/cluster_import.go
  16. 39
      pkg/cluster/cluster_scan.go
  17. 171
      pkg/cluster/env.go
  18. 15
      pkg/cluster/errors.go
  19. 6
      pkg/cluster/hosts.go
  20. 17
      pkg/cluster/machine.go
  21. 124
      pkg/cluster/machine_rings.go
  22. 14
      pkg/cluster/machine_scan.go
  23. 232
      pkg/cluster/regions.go
  24. 250
      pkg/cluster/rings.go
  25. 6
      pkg/cluster/sync.go
  26. 90
      pkg/cluster/wireguard.go
  27. 46
      pkg/cluster/zones.go
  28. 77
      pkg/rings/cidr.go
  29. 178
      pkg/rings/cidr_test.go
  30. 122
      pkg/rings/decode.go
  31. 53
      pkg/rings/decode_test.go
  32. 154
      pkg/rings/encode.go
  33. 63
      pkg/rings/encode_test.go
  34. 116
      pkg/rings/rings.go
  35. 72
      pkg/tools/buffer.go
  36. 3
      pkg/tools/gen_index.sh
  37. 140
      pkg/tools/gen_mk.sh
  38. 2
      pkg/tools/revive.toml
  39. 14
      pkg/tools/tools.go
  40. 2
      pkg/wireguard/config.go
  41. 100
      pkg/wireguard/config_parser.go

1
.gitignore vendored

@ -1,2 +1 @@
.tmp .tmp
.version

29
Makefile

@ -6,20 +6,19 @@ GOFMT ?= gofmt
GOFMT_FLAGS = -w -l -s GOFMT_FLAGS = -w -l -s
GOGENERATE_FLAGS = -v GOGENERATE_FLAGS = -v
TOOLSDIR := $(CURDIR)/pkg/tools GOPATH ?= $(shell $(GO) env GOPATH)
TMPDIR ?= $(CURDIR)/.tmp GOBIN ?= $(GOPATH)/bin
OUTDIR ?= $(TMPDIR)
GOLANGCI_LINT_VERSION ?= v1.59.1
REVIVE_VERSION ?= v1.3.7
GOLANGCI_LINT_URL ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) TOOLSDIR := $(CURDIR)/pkg/tools
GOLANGCI_LINT ?= $(GO) run $(GOLANGCI_LINT_URL) TMPDIR ?= .tmp
REVIVE ?= $(GOBIN)/revive
REVIVE_CONF ?= $(TOOLSDIR)/revive.toml REVIVE_CONF ?= $(TOOLSDIR)/revive.toml
REVIVE_RUN_ARGS ?= -config $(REVIVE_CONF) -formatter friendly REVIVE_RUN_ARGS ?= -config $(REVIVE_CONF) -formatter friendly
REVIVE_URL ?= github.com/mgechev/revive@$(REVIVE_VERSION) REVIVE_INSTALL_URL ?= github.com/mgechev/revive
REVIVE ?= $(GO) run $(REVIVE_URL)
GO_INSTALL_URLS = \
$(REVIVE_INSTALL_URL) \
V = 0 V = 0
Q = $(if $(filter 1,$V),,@) Q = $(if $(filter 1,$V),,@)
@ -30,12 +29,11 @@ GO_BUILD_CMD = $(GO_BUILD) -o "$(OUTDIR)"
all: get generate tidy build all: get generate tidy build
clean: ; $(info $(M) cleaning) install:
rm -rf $(TMPDIR)
install: ; $(info $(M) cleaning)
$Q $(GO) install -v ./cmd/... $Q $(GO) install -v ./cmd/...
clean: ; $(info $(M) cleaning)
rm -rf $(TMPDIR)
$(TMPDIR)/index: $(TOOLSDIR)/gen_index.sh Makefile FORCE ; $(info $(M) generating index) $(TMPDIR)/index: $(TOOLSDIR)/gen_index.sh Makefile FORCE ; $(info $(M) generating index)
$Q mkdir -p $(@D) $Q mkdir -p $(@D)
@ -56,3 +54,6 @@ tidy: fmt
generate: ; $(info $(M) running go:generate) generate: ; $(info $(M) running go:generate)
$Q git grep -l '^//go:generate' | sort -uV | xargs -r -n1 $(GO) generate $(GOGENERATE_FLAGS) $Q git grep -l '^//go:generate' | sort -uV | xargs -r -n1 $(GO) generate $(GOGENERATE_FLAGS)
$(REVIVE):
$Q $(GO) install -v $(REVIVE_INSTALL_URL)

2
cmd/jpictl/dns.go

@ -52,7 +52,7 @@ func populateDNSManager(mgr *dns.Manager, m *cluster.Cluster) error {
m.ForEachZone(func(z *cluster.Zone) bool { m.ForEachZone(func(z *cluster.Zone) bool {
z.ForEachMachine(func(p *cluster.Machine) bool { z.ForEachMachine(func(p *cluster.Machine) bool {
err = mgr.AddHost(ctx, z.Name, int(p.ID), p.IsActive(), p.PublicAddresses...) err = mgr.AddHost(ctx, z.Name, p.ID, p.IsActive(), p.PublicAddresses...)
return err != nil return err != nil
}) })

9
cmd/jpictl/gateway.go

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -43,7 +44,7 @@ func gatewaySet(zi cluster.ZoneIterator, gw string) error {
zi.ForEachZone(func(z *cluster.Zone) bool { zi.ForEachZone(func(z *cluster.Zone) bool {
for _, m := range z.Machines { for _, m := range z.Machines {
if m.Name == gw { if m.Name == gw {
_ = z.SetGateway(m.ID, true) z.SetGateway(m.ID, true)
return true return true
} }
} }
@ -79,8 +80,8 @@ func gatewayUnset(zi cluster.ZoneIterator, ngw string) error {
zi.ForEachZone(func(z *cluster.Zone) bool { zi.ForEachZone(func(z *cluster.Zone) bool {
for _, m := range z.Machines { for _, m := range z.Machines {
if m.Name == ngw && m.IsGateway() { if m.Name == ngw && m.IsGateway() {
_ = z.SetGateway(m.ID, false) z.SetGateway(m.ID, false)
_ = m.RemoveWireguardConfig(0) m.RemoveWireguardConfig(0)
return true return true
} }
} }
@ -127,7 +128,7 @@ func gatewayListAll(zi cluster.ZoneIterator) error {
return false return false
} }
for _, i := range ids { for _, i := range ids {
sIDs = append(sIDs, i.String()) sIDs = append(sIDs, strconv.Itoa(i))
} }
b.WriteString(strings.Join(sIDs, ", ")) b.WriteString(strings.Join(sIDs, ", "))
b.WriteString("\n") b.WriteString("\n")

200
cmd/jpictl/list.go

@ -1,200 +0,0 @@
package main
import (
"bytes"
"io"
"net/netip"
"os"
"darvaza.org/core"
"github.com/spf13/cobra"
"git.jpi.io/amery/jpictl/pkg/cluster"
"git.jpi.io/amery/jpictl/pkg/rings"
"git.jpi.io/amery/jpictl/pkg/tools"
)
type inventory struct {
r []*cluster.Region
z [][]*cluster.Zone
}
func (g *inventory) renderRingZero(out *tools.LazyBuffer) error {
ring0 := netip.PrefixFrom(rings.UnsafeRingZeroAddress(0, 0, 0), rings.RingZeroBits)
from, to, _ := rings.PrefixToRange(ring0)
_ = out.Printf("; wg%v\n", 0)
_ = out.Printf("%s\t%s-%s\n", ring0, from, to)
if err := g.renderRingZeroRegions(out); err != nil {
return err
}
return g.renderRingZeroZones(out)
}
func (g *inventory) renderRingZeroRegions(out *tools.LazyBuffer) error {
for _, r := range g.r {
if err := g.renderRingZeroRegion(out, r); err != nil {
return err
}
}
return nil
}
func (*inventory) renderRingZeroRegion(out *tools.LazyBuffer, r *cluster.Region) error {
addr := rings.UnsafeRingZeroAddress(r.ID, 0, 0)
ring0r := netip.PrefixFrom(addr, rings.RingZeroBits+4)
from, to, _ := rings.PrefixToRange(ring0r)
_ = out.Printf("%s\t%s-%s\t# %s\n", ring0r, from, to, r.Name)
return nil
}
func (g *inventory) renderRingZeroZones(out *tools.LazyBuffer) error {
for i, r := range g.r {
for _, z := range g.z[i] {
if err := g.renderRingZeroZone(out, r, z); err != nil {
return err
}
}
}
return nil
}
func (*inventory) renderRingZeroZone(out *tools.LazyBuffer, r *cluster.Region, z *cluster.Zone) error {
addr := rings.UnsafeRingZeroAddress(r.ID, z.ID, 0)
ring0rz := netip.PrefixFrom(addr, rings.RingZeroBits+4+4)
from, to, _ := rings.PrefixToRange(ring0rz)
_ = out.Printf("; wg%v: %s (%s)\n", 0, z.Name, r.Name)
_ = out.Printf("%s\t%s-%s\t%s\n", ring0rz, from, to, z.Name)
z.ForEachMachine(func(m *cluster.Machine) bool {
if m.IsGateway() {
addr, _ := m.RingZeroAddress()
cidr := netip.PrefixFrom(addr, 32)
_ = out.Printf("%s\t\t%s-%v\n", cidr, m.Name, 0)
}
return false
})
return nil
}
func (g *inventory) renderRingOne(out *tools.LazyBuffer) error {
for i, r := range g.r {
for _, z := range g.z[i] {
if err := g.renderRingOneZone(out, r, z); err != nil {
return err
}
}
}
return nil
}
func (*inventory) renderRingOneZone(out *tools.LazyBuffer, r *cluster.Region, z *cluster.Zone) error {
ring1, err := rings.RingOnePrefix(r.ID, z.ID)
if err != nil {
return err
}
from, to, _ := rings.PrefixToRange(ring1)
_ = out.Printf("; wg%v: %s (%s)\n", 1, z.Name, r.Name)
_ = out.Printf("%s\t%s-%s\t%s\n", ring1, from, to, z.Name)
z.ForEachMachine(func(m *cluster.Machine) bool {
addr := m.RingOneAddress()
cidr := netip.PrefixFrom(addr, 32)
_ = out.Printf("%s\t\t%s\n", cidr, m.Name)
return false
})
return nil
}
func (g *inventory) Marshal() ([]byte, error) {
var buf tools.LazyBuffer
if err := g.renderRingZero(&buf); err != nil {
return nil, err
}
if err := g.renderRingOne(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (g *inventory) WriteTo(out io.Writer) (int64, error) {
b, err := g.Marshal()
if err != nil {
return 0, err
}
buf := bytes.NewBuffer(b)
return buf.WriteTo(out)
}
func genInventory(m *cluster.Cluster) (*inventory, error) {
g := new(inventory)
g.populateRegions(m)
g.populateZones()
return g, nil
}
func (g *inventory) populateRegions(m *cluster.Cluster) {
m.ForEachRegion(func(r *cluster.Region) bool {
if r.IsPrimary() {
g.r = append(g.r, r)
}
return false
})
core.SliceSortFn(g.r, func(a, b *cluster.Region) bool {
return a.ID < b.ID
})
}
func (g *inventory) populateZones() {
g.z = make([][]*cluster.Zone, len(g.r))
for i, r := range g.r {
r.ForEachZone(func(z *cluster.Zone) bool {
g.z[i] = append(g.z[i], z)
return false
})
core.SliceSortFn(g.z[i], func(a, b *cluster.Zone) bool {
return a.ID < b.ID
})
}
}
// Command
var listCmd = &cobra.Command{
Use: "list",
Short: "list shows the IP/CIDR inventory",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(false)
if err != nil {
return err
}
out, err := genInventory(m)
if err != nil {
return err
}
_, err = out.WriteTo(os.Stdout)
return err
},
}
func init() {
rootCmd.AddCommand(listCmd)
}

7
cmd/jpictl/main.go

@ -2,8 +2,6 @@
package main package main
import ( import (
_ "embed"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,9 +12,8 @@ const (
var ( var (
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: CmdName, Use: CmdName,
Short: "control tool for jpi.cloud", Short: "control tool for jpi.cloud",
Version: version,
} }
) )

31
cmd/jpictl/version.go

@ -1,31 +0,0 @@
package main
import (
_ "embed"
"fmt"
"os"
"github.com/spf13/cobra"
)
//go:generate sh -c "git describe | tr -d '\r\n' > .version"
//go:embed .version
var version string
var versionCmd = &cobra.Command{
Use: "version",
Short: "Returns jpictl's version",
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, _ []string) {
_, _ = fmt.Fprintf(os.Stdout, "%s\n", version)
},
}
func init() {
if version == "" {
version = "undetermined"
}
rootCmd.AddCommand(versionCmd)
}

53
go.mod

@ -1,43 +1,54 @@
module git.jpi.io/amery/jpictl module git.jpi.io/amery/jpictl
go 1.21 go 1.19
replace asciigoat.org/ini => ../../../asciigoat.org/ini
require ( require (
asciigoat.org/core v0.3.9 // indirect
asciigoat.org/ini v0.2.5 asciigoat.org/ini v0.2.5
darvaza.org/cache/x/simplelru v0.1.8 // indirect darvaza.org/core v0.12.0
darvaza.org/core v0.14.2
darvaza.org/resolver v0.9.2 darvaza.org/resolver v0.9.2
darvaza.org/sidecar v0.4.0 darvaza.org/sidecar v0.4.0
darvaza.org/slog v0.5.7 darvaza.org/slog v0.5.7
darvaza.org/slog/handlers/discard v0.4.11 darvaza.org/slog/handlers/discard v0.4.11
darvaza.org/slog/handlers/filter v0.4.9 // indirect github.com/gofrs/uuid/v5 v5.0.0
darvaza.org/slog/handlers/zerolog v0.4.9 // indirect
)
require (
github.com/gofrs/uuid/v5 v5.2.0
github.com/hack-pad/hackpadfs v0.2.1 github.com/hack-pad/hackpadfs v0.2.1
github.com/libdns/cloudflare v0.1.1 github.com/libdns/cloudflare v0.1.0
github.com/libdns/libdns v0.2.2 github.com/libdns/libdns v0.2.1
github.com/mgechev/revive v1.3.7
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.25.0 golang.org/x/crypto v0.20.0
golang.org/x/net v0.27.0 golang.org/x/net v0.21.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
asciigoat.org/core v0.3.9 // indirect
darvaza.org/cache/x/simplelru v0.1.8 // indirect
darvaza.org/slog/handlers/filter v0.4.9 // indirect
darvaza.org/slog/handlers/zerolog v0.4.9 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.59 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/zerolog v1.33.0 // indirect github.com/rs/zerolog v1.32.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.15.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.18.0 // indirect
) )

95
go.sum

@ -1,11 +1,9 @@
asciigoat.org/core v0.3.9 h1:hgDDz4ecm3ZvehX++m8A/IzAt+B5oDPiRtxatzfUHPQ= asciigoat.org/core v0.3.9 h1:hgDDz4ecm3ZvehX++m8A/IzAt+B5oDPiRtxatzfUHPQ=
asciigoat.org/core v0.3.9/go.mod h1:CAaHwyw8MpAq4a1MYtN2dxJrsK+hmIdW50OndaQZYPI= asciigoat.org/core v0.3.9/go.mod h1:CAaHwyw8MpAq4a1MYtN2dxJrsK+hmIdW50OndaQZYPI=
asciigoat.org/ini v0.2.5 h1:4gRIp9rU+XQt8+HMqZO5R7GavMv9Yl2+N+je6djDIAE=
asciigoat.org/ini v0.2.5/go.mod h1:gmXzJ9XFqf1NLk5nQkj04USQ4tMtdRJHNQX6vp3DzjU=
darvaza.org/cache/x/simplelru v0.1.8 h1:rvFucut4wKYbsYc994yR3P0M08NqlsvZxr5G4QK82tw= darvaza.org/cache/x/simplelru v0.1.8 h1:rvFucut4wKYbsYc994yR3P0M08NqlsvZxr5G4QK82tw=
darvaza.org/cache/x/simplelru v0.1.8/go.mod h1:Mv1isOJTcXYK+aK0AvUe+/3KpRTXDsYga6rdTS/upNs= darvaza.org/cache/x/simplelru v0.1.8/go.mod h1:Mv1isOJTcXYK+aK0AvUe+/3KpRTXDsYga6rdTS/upNs=
darvaza.org/core v0.14.2 h1:6p0iznuGfVGbBp+CnkZTw1b76j6Q/j4ffDztZXrrlK8= darvaza.org/core v0.12.0 h1:LLtYh9RZJSd0sgPTDocofCc4H5jbutSUQgPbKt+8gcI=
darvaza.org/core v0.14.2/go.mod h1:C+B0GRNLB+/asGfxjQ9XZERdk7xaFxzt5xTIBPiNm2M= darvaza.org/core v0.12.0/go.mod h1:47Ydh67KnzjLNu1mzX3r2zpphbxQqEaihMsUq5GflQ4=
darvaza.org/resolver v0.9.2 h1:sUX6LZ1eN5TzJW7L4m7HM+BvwBeWl8dYYDGVSe+AIhk= darvaza.org/resolver v0.9.2 h1:sUX6LZ1eN5TzJW7L4m7HM+BvwBeWl8dYYDGVSe+AIhk=
darvaza.org/resolver v0.9.2/go.mod h1:XWqPhrxoOKNzRuSozOwmE1M6QVqQL28jEdxylnIO8Nw= darvaza.org/resolver v0.9.2/go.mod h1:XWqPhrxoOKNzRuSozOwmE1M6QVqQL28jEdxylnIO8Nw=
darvaza.org/sidecar v0.4.0 h1:wHghxzLsiT82WDBBUf34aTqtOvRBg4UbxVIJgKNXRVA= darvaza.org/sidecar v0.4.0 h1:wHghxzLsiT82WDBBUf34aTqtOvRBg4UbxVIJgKNXRVA=
@ -18,12 +16,23 @@ darvaza.org/slog/handlers/filter v0.4.9 h1:xD8OBwlJytpiwTSDDZqUuNSOsJuaManXQiOj9
darvaza.org/slog/handlers/filter v0.4.9/go.mod h1:t+sjcf1c46kAdf1TRiQmop91xlkteZrC4WDXoVwHgP8= darvaza.org/slog/handlers/filter v0.4.9/go.mod h1:t+sjcf1c46kAdf1TRiQmop91xlkteZrC4WDXoVwHgP8=
darvaza.org/slog/handlers/zerolog v0.4.9 h1:08FjRnwRGtJsLLBnbgxVorb/bkgm5QEM/LXD2cxeCbM= darvaza.org/slog/handlers/zerolog v0.4.9 h1:08FjRnwRGtJsLLBnbgxVorb/bkgm5QEM/LXD2cxeCbM=
darvaza.org/slog/handlers/zerolog v0.4.9/go.mod h1:PZYfx6eOxQfD+cXJQp52iwKgcD30QVYHoXxOCojAOdw= darvaza.org/slog/handlers/zerolog v0.4.9/go.mod h1:PZYfx6eOxQfD+cXJQp52iwKgcD30QVYHoXxOCojAOdw=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw= github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw=
github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU= github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -32,50 +41,78 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU= github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0=
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE=
github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

3
pkg/ceph/config.go

@ -39,9 +39,6 @@ func writeGlobalToBuffer(w *bytes.Buffer, c *GlobalConfig) {
_, _ = fmt.Fprintf(w, "%s = %s\n", "mon_initial_members", strings.Join(c.Monitors, ", ")) _, _ = fmt.Fprintf(w, "%s = %s\n", "mon_initial_members", strings.Join(c.Monitors, ", "))
_, _ = fmt.Fprintf(w, "%s = %s\n", "mon_host", joinAddrs(c.MonitorsAddr, ", ")) _, _ = fmt.Fprintf(w, "%s = %s\n", "mon_host", joinAddrs(c.MonitorsAddr, ", "))
_, _ = fmt.Fprintf(w, "%s = %s\n", "cluster_network", c.ClusterNetwork.String()) _, _ = fmt.Fprintf(w, "%s = %s\n", "cluster_network", c.ClusterNetwork.String())
_, _ = fmt.Fprintf(w, "\n; %s\n", "don't rewrite labels on startup")
_, _ = fmt.Fprintf(w, "%s = %s\n", "osd_class_update_on_start", "false")
} }
func joinAddrs(addrs []netip.Addr, sep string) string { func joinAddrs(addrs []netip.Addr, sep string) string {

59
pkg/ceph/config_parser.go

@ -2,10 +2,9 @@ package ceph
import ( import (
"io/fs" "io/fs"
"net/netip" "log"
"asciigoat.org/ini/basic" "asciigoat.org/ini/basic"
"asciigoat.org/ini/parser"
"darvaza.org/core" "darvaza.org/core"
) )
@ -36,61 +35,27 @@ func loadGlobalConfSection(out *Config, src *basic.Section) error {
return nil return nil
} }
// revive:disable:cyclomatic
// revive:disable:cognitive-complexity
func loadGlobalConfField(cfg *GlobalConfig, field basic.Field) error { func loadGlobalConfField(cfg *GlobalConfig, field basic.Field) error {
// revive:enable:cyclomatic log.Printf("%s[%q] = %q", "global", field.Key, field.Value)
// revive:enable:cognitive-complexity
// TODO: refactor when asciigoat's ini parser learns to do reflection key, value := field.Key, field.Value
switch field.Key { switch key {
case "fsid": case "fsid":
if !core.IsZero(cfg.FSID) { return configFieldHandler(&cfg.FSID, key, value)
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.FSID.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "mon_host": case "mon_host":
entries, _ := parser.SplitCommaArray(field.Value) return configFieldHandler(&cfg.MonitorsAddr, key, value)
for _, s := range entries {
var addr netip.Addr
if err := addr.UnmarshalText([]byte(s)); err != nil {
return core.Wrap(err, field.Key)
}
cfg.MonitorsAddr = append(cfg.MonitorsAddr, addr)
}
return nil
case "mon_initial_members": case "mon_initial_members":
entries, _ := parser.SplitCommaArray(field.Value) return configFieldHandler(&cfg.Monitors, key, value)
cfg.Monitors = append(cfg.Monitors, entries...)
return nil
case "cluster_network": case "cluster_network":
if !core.IsZero(cfg.ClusterNetwork) { return configFieldHandler(&cfg.ClusterNetwork, key, value)
err := core.Wrap(fs.ErrInvalid, "fields before the first section") default:
return err return core.Wrap(fs.ErrNotExist, "field %q unknown", key)
}
err := cfg.ClusterNetwork.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
} }
return nil
} }
func configFieldHandler(vi any, key, value string) error
func newConfigFromDocument(doc *basic.Document) (*Config, error) { func newConfigFromDocument(doc *basic.Document) (*Config, error) {
var out Config var out Config

43
pkg/cluster/addr.go

@ -1,43 +0,0 @@
package cluster
import (
"net/netip"
"git.jpi.io/amery/jpictl/pkg/rings"
)
// RingOnePrefix returns the ring 1 subnet of this [Zone].
func (z *Zone) RingOnePrefix() netip.Prefix {
subnet, err := rings.RingOnePrefix(z.RegionID(), z.ID)
if err != nil {
panic(err)
}
return subnet
}
// RingOnePrefix returns the ring 1 subnet this [Machine] belongs
// to.
func (m *Machine) RingOnePrefix() netip.Prefix {
return m.zone.RingOnePrefix()
}
// RingZeroAddress returns the ring 0 address of the [Machine]
// if it can act as gateway.
func (m *Machine) RingZeroAddress() (netip.Addr, bool) {
addr, err := rings.RingZeroAddress(m.Region(), m.Zone(), m.ID)
if err != nil {
return netip.Addr{}, false
}
return addr, true
}
// RingOneAddress returns the ring 1 address of the [Machine]
func (m *Machine) RingOneAddress() netip.Addr {
addr, err := rings.RingOneAddress(m.Region(), m.Zone(), m.ID)
if err != nil {
panic(err)
}
return addr
}

2
pkg/cluster/ceph.go

@ -66,7 +66,7 @@ func (m *Cluster) GenCephConfig() (*ceph.Config, error) {
m.ForEachZone(func(z *Zone) bool { m.ForEachZone(func(z *Zone) bool {
for _, p := range z.GetCephMonitors() { for _, p := range z.GetCephMonitors() {
addr := p.RingOneAddress() addr, _ := RingOneAddress(z.ID, p.ID)
cfg.Global.Monitors = append(cfg.Global.Monitors, p.Name) cfg.Global.Monitors = append(cfg.Global.Monitors, p.Name)
cfg.Global.MonitorsAddr = append(cfg.Global.MonitorsAddr, addr) cfg.Global.MonitorsAddr = append(cfg.Global.MonitorsAddr, addr)

9
pkg/cluster/ceph_scan.go

@ -4,7 +4,6 @@ import (
"os" "os"
"darvaza.org/slog" "darvaza.org/slog"
"git.jpi.io/amery/jpictl/pkg/ceph" "git.jpi.io/amery/jpictl/pkg/ceph"
) )
@ -15,7 +14,8 @@ type cephScanTODO struct {
func (todo *cephScanTODO) checkMachine(p *Machine) bool { func (todo *cephScanTODO) checkMachine(p *Machine) bool {
// on ceph all addresses are ring1 // on ceph all addresses are ring1
addr := p.RingOneAddress().String() ring1, _ := RingOneAddress(p.Zone(), p.ID)
addr := ring1.String()
if _, found := todo.names[p.Name]; found { if _, found := todo.names[p.Name]; found {
// found on the TODO by name // found on the TODO by name
@ -73,7 +73,10 @@ func newCephScanTODO(cfg *ceph.Config) *cephScanTODO {
func (m *Cluster) scanCephMonitors(opts *ScanOptions) error { func (m *Cluster) scanCephMonitors(opts *ScanOptions) error {
cfg, err := m.GetCephConfig() cfg, err := m.GetCephConfig()
if err != nil && !os.IsNotExist(err) { switch {
case os.IsNotExist(err):
err = nil
case err != nil:
return err return err
} }

8
pkg/cluster/cluster_import.go

@ -6,18 +6,16 @@ import (
"os" "os"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
func (m *Cluster) init(opts *ScanOptions) error { func (m *Cluster) init(opts *ScanOptions) error {
for _, fn := range []func(*ScanOptions) error{ for _, fn := range []func(*ScanOptions) error{
m.initZones, m.initZones,
m.initRegions,
m.scanZoneIDs, m.scanZoneIDs,
m.scanSort, m.scanSort,
m.scanGateways, m.scanGateways,
m.initCephMonitors, m.initCephMonitors,
m.initRegions,
} { } {
if err := fn(opts); err != nil { if err := fn(opts); err != nil {
return err return err
@ -47,7 +45,7 @@ func (m *Cluster) initZones(opts *ScanOptions) error {
func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error { func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error {
var hasMissing bool var hasMissing bool
var lastMachineID rings.NodeID var lastMachineID int
z.zones = m z.zones = m
z.logger = m z.logger = m
@ -60,7 +58,7 @@ func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error {
case p.ID == 0: case p.ID == 0:
hasMissing = true hasMissing = true
case p.ID > lastMachineID: case p.ID > lastMachineID:
lastMachineID = p.ID lastMachineID = z.ID
} }
return false return false

39
pkg/cluster/cluster_scan.go

@ -4,30 +4,23 @@ import (
"io/fs" "io/fs"
"path" "path"
"sort" "sort"
"strings"
"darvaza.org/core" "darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
const ( const (
// ZoneRegionsFileName indicates the file containing // ZoneRegionsFileName indicates the file containing
// region names as references // region names as references
ZoneRegionsFileName = "regions" ZoneRegionsFileName = "regions"
// RegionClusterTokenFileName contains the kubernetes
// token of the cluster this region represents
RegionClusterTokenFileName = "k8s_token"
) )
func (m *Cluster) scan(opts *ScanOptions) error { func (m *Cluster) scan(opts *ScanOptions) error {
for _, fn := range []func(*ScanOptions) error{ for _, fn := range []func(*ScanOptions) error{
m.scanDirectory, m.scanDirectory,
m.scanMachines, m.scanMachines,
m.initRegions,
m.scanZoneIDs, m.scanZoneIDs,
m.scanSort, m.scanSort,
m.initRegions,
m.scanGateways, m.scanGateways,
m.scanCephMonitors, m.scanCephMonitors,
} { } {
@ -116,7 +109,7 @@ func (m *Cluster) scanMachines(opts *ScanOptions) error {
func (m *Cluster) scanZoneIDs(_ *ScanOptions) error { func (m *Cluster) scanZoneIDs(_ *ScanOptions) error {
var hasMissing bool var hasMissing bool
var lastZoneID rings.ZoneID var lastZoneID int
m.ForEachZone(func(z *Zone) bool { m.ForEachZone(func(z *Zone) bool {
switch { switch {
@ -193,8 +186,6 @@ func (z *Zone) scan() error {
switch { switch {
case name == ZoneRegionsFileName: case name == ZoneRegionsFileName:
err = z.loadRegions() err = z.loadRegions()
case name == RegionClusterTokenFileName:
err = z.loadClusterToken()
case e.IsDir(): case e.IsDir():
err = z.scanSubdirectory(name) err = z.scanSubdirectory(name)
default: default:
@ -227,32 +218,6 @@ func (z *Zone) loadRegions() error {
return err return err
} }
func (z *Zone) loadClusterToken() error {
var token string
filename := path.Join(z.Name, RegionClusterTokenFileName)
lines, err := z.zones.ReadLines(filename)
if err != nil {
return err
}
// first non-empty line
for _, s := range lines {
s = strings.TrimSpace(s)
if s != "" {
token = s
break
}
}
err = z.zones.setRegionClusterToken(z.Name, token)
if err != nil {
err = core.Wrap(err, filename)
}
return err
}
func (z *Zone) scanSubdirectory(name string) error { func (z *Zone) scanSubdirectory(name string) error {
m := &Machine{ m := &Machine{
zone: z, zone: z,

171
pkg/cluster/env.go

@ -5,16 +5,11 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
// Env is a shell environment factory for this cluster // Env is a shell environment factory for this cluster
type Env struct { type Env struct {
ZoneIterator ZoneIterator
RegionIterator
cephFSID string cephFSID string
export bool export bool
@ -28,61 +23,26 @@ func (m *Cluster) Env(export bool) (*Env, error) {
} }
env := &Env{ env := &Env{
ZoneIterator: m, ZoneIterator: m,
RegionIterator: m, cephFSID: fsid.String(),
cephFSID: fsid.String(), export: export,
export: export,
} }
return env, nil return env, nil
} }
// Zones returns the list of Zone IDs of a region, // Zones returns the list of Zone IDs
// or from all if none is specified. func (m *Env) Zones() []int {
func (m *Env) Zones(r *Region) []rings.ZoneID { var zones []int
var zones []rings.ZoneID
iter := core.IIf[ZoneIterator](r != nil, r, m) m.ForEachZone(func(z *Zone) bool {
iter.ForEachZone(func(z *Zone) bool {
zones = append(zones, z.ID) zones = append(zones, z.ID)
return false return false
}) })
core.SliceSortOrdered(zones)
return zones return zones
} }
// RegionsNames returns a sorted list of primary regions names
func (m *Env) RegionsNames() []string {
var regions []string
m.ForEachRegion(func(r *Region) bool {
if r.IsPrimary() {
regions = append(regions, r.Name)
}
return false
})
core.SliceSortOrdered(regions)
return regions
}
// Regions returns a sorted list of primary regions IDs
func (m *Env) Regions() (regions []rings.RegionID) {
m.ForEachRegion(func(r *Region) bool {
if r.IsPrimary() {
regions = append(regions, r.ID)
}
return false
})
core.SliceSortOrdered(regions)
return regions
}
// WriteTo generates environment variables for shell scripts // WriteTo generates environment variables for shell scripts
func (m *Env) WriteTo(w io.Writer) (int64, error) { func (m *Env) WriteTo(w io.Writer) (int64, error) {
var buf bytes.Buffer var buf bytes.Buffer
@ -91,75 +51,56 @@ func (m *Env) WriteTo(w io.Writer) (int64, error) {
m.writeEnvVar(&buf, m.cephFSID, "FSID") m.writeEnvVar(&buf, m.cephFSID, "FSID")
} }
regions := m.getRegions() m.writeEnvVarInts(&buf, m.Zones(), "ZONES")
ids := core.SliceMap(regions, func(_ []rings.RegionID, r *Region) (out []rings.RegionID) {
return append(out, r.ID)
})
names := core.SliceMap(regions, func(_ []string, r *Region) (out []string) {
return append(out, r.Name)
})
m.writeEnvVar(&buf, genEnvInts(ids), "REGIONS")
m.writeEnvVar(&buf, genEnvStrings(names), "REGIONS_NAMES")
for _, r := range regions { m.ForEachZone(func(z *Zone) bool {
m.writeEnvRegion(&buf, r) m.writeEnvZone(&buf, z)
} return false
})
return buf.WriteTo(w) return buf.WriteTo(w)
} }
func (m *Env) getRegions() (out []*Region) { func (m *Env) writeEnvZone(w io.Writer, z *Zone) {
m.ForEachRegion(func(r *Region) bool { zoneID := z.ID
if r.IsPrimary() {
out = append(out, r)
}
return false
})
core.SliceSortFn(out, func(a, b *Region) bool {
return a.ID < b.ID
})
return out // ZONE{zoneID}
} m.writeEnvVar(w, genEnvZoneNodes(z), "ZONE%v", zoneID)
func (m *Env) writeEnvRegion(w io.Writer, r *Region) {
regionID := r.ID
// REGION{regionID}_NAME // ZONE{zoneID}_NAME
m.writeEnvVar(w, r.Name, "REGION%v_%s", regionID, "NAME") m.writeEnvVar(w, z.Name, "ZONE%v_%s", zoneID, "NAME")
// REGION{regionID}_ZONES // ZONE{zoneID}_GW
m.writeEnvVar(w, genEnvInts(m.Zones(r)), "REGION%v_%s", regionID, "ZONES") gateways, _ := z.GatewayIDs()
m.writeEnvVarInts(w, gateways, "ZONE%v_%s", zoneID, "GW")
r.ForEachZone(func(z *Zone) bool { // Ceph
m.writeEnvZone(w, r, z) monitors := z.GetCephMonitors()
return false // MON{zoneID}_NAME
}) m.writeEnvVar(w, genEnvZoneCephMonNames(monitors), "MON%v_%s", zoneID, "NAME")
// MON{zoneID}_IP
m.writeEnvVar(w, genEnvZoneCephMonIPs(monitors), "MON%v_%s", zoneID, "IP")
// MON{zoneID}_ID
m.writeEnvVar(w, genEnvZoneCephMonIDs(monitors), "MON%v_%s", zoneID, "ID")
} }
func (m *Env) writeEnvZone(w io.Writer, r *Region, z *Zone) { func (m *Env) writeEnvVarInts(w io.Writer, value []int, name string, args ...any) {
zonePrefix := fmt.Sprintf("REGION%v_ZONE%v", r.ID, z.ID) var s string
monPrefix := zonePrefix + "_MON"
// REGION{regionID}_ZONE{zoneID} if n := len(value); n > 0 {
m.writeEnvVar(w, genEnvZoneNodes(z), zonePrefix) var buf bytes.Buffer
// REGION{regionID}_ZONE{zoneID}_NAME for i, v := range value {
m.writeEnvVar(w, z.Name, zonePrefix+"_NAME") if i != 0 {
_, _ = fmt.Fprint(&buf, " ")
}
_, _ = fmt.Fprintf(&buf, "%v", v)
}
// REGION{regionID}_ZONE{zoneID}_GW s = buf.String()
gateways, _ := z.GatewayIDs() }
m.writeEnvVar(w, genEnvInts(gateways), zonePrefix+"_GW")
// Ceph m.writeEnvVar(w, s, name, args...)
monitors := z.GetCephMonitors()
// REGION{regionID}_MON{zone_ID}
m.writeEnvVar(w, genEnvZoneCephMonNames(monitors), monPrefix)
// REGION{regionID}_MON{zone_ID}_IP
m.writeEnvVar(w, genEnvZoneCephMonIPs(monitors), monPrefix+"_IP")
// REGION{regionID}_MON{zone_ID}_ID
m.writeEnvVar(w, genEnvZoneCephMonIDs(monitors), monPrefix+"_ID")
} }
func (m *Env) writeEnvVar(w io.Writer, value string, name string, args ...any) { func (m *Env) writeEnvVar(w io.Writer, value string, name string, args ...any) {
@ -176,32 +117,10 @@ func (m *Env) writeEnvVar(w io.Writer, value string, name string, args ...any) {
if name != "" { if name != "" {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { _, _ = fmt.Fprintf(w, "%s%s=%q\n", prefix, name, value)
_, _ = fmt.Fprintf(w, "%s%s=\n", prefix, name)
} else {
_, _ = fmt.Fprintf(w, "%s%s=%q\n", prefix, name, value)
}
} }
} }
func genEnvInts[T core.Signed](values []T) string {
var buf bytes.Buffer
for _, v := range values {
if buf.Len() > 0 {
_, _ = buf.WriteRune(' ')
}
_, _ = buf.WriteString(fmt.Sprintf("%v", int64(v)))
}
return buf.String()
}
func genEnvStrings(values []string) string {
return strings.Join(values, " ")
}
func genEnvZoneNodes(z *Zone) string { func genEnvZoneNodes(z *Zone) string {
if n := z.Len(); n > 0 { if n := z.Len(); n > 0 {
s := make([]string, 0, n) s := make([]string, 0, n)
@ -211,7 +130,7 @@ func genEnvZoneNodes(z *Zone) string {
return false return false
}) })
return genEnvStrings(s) return strings.Join(s, " ")
} }
return "" return ""
} }
@ -232,7 +151,7 @@ func genEnvZoneCephMonNames(m Machines) string {
func genEnvZoneCephMonIPs(m Machines) string { func genEnvZoneCephMonIPs(m Machines) string {
var buf strings.Builder var buf strings.Builder
m.ForEachMachine(func(p *Machine) bool { m.ForEachMachine(func(p *Machine) bool {
addr := p.RingOneAddress() addr, _ := RingOneAddress(p.Zone(), p.ID)
if buf.Len() > 0 { if buf.Len() > 0 {
_, _ = buf.WriteRune(' ') _, _ = buf.WriteRune(' ')

15
pkg/cluster/errors.go

@ -1,13 +1,6 @@
package cluster package cluster
import ( import "errors"
"errors"
"io/fs"
"darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/rings"
)
var ( var (
// ErrInvalidName indicates the name isn't valid // ErrInvalidName indicates the name isn't valid
@ -21,9 +14,3 @@ var (
// the intended purpose // the intended purpose
ErrInvalidNode = errors.New("invalid node") ErrInvalidNode = errors.New("invalid node")
) )
// ErrInvalidRing returns an error indicating the [rings.RingID]
// can't be used for the intended purpose
func ErrInvalidRing(ringID rings.RingID) error {
return core.QuietWrap(fs.ErrInvalid, "invalid ring %v", ringID-1)
}

6
pkg/cluster/hosts.go

@ -71,14 +71,14 @@ func (p *Machine) WriteHosts() error {
func (z *Zone) genHosts(out *hostsFile, p *Machine) { func (z *Zone) genHosts(out *hostsFile, p *Machine) {
var names []string var names []string
ip := p.RingOneAddress() ip, _ := RingOneAddress(p.zone.ID, p.ID)
names = append(names, p.Name) names = append(names, p.Name)
if p.CephMonitor { if p.CephMonitor {
names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "ceph")) names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "ceph"))
names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "k3s")) names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "k3s"))
if z.Is(p.Region(), p.Zone()) { if z.ID == p.zone.ID {
names = append(names, "ceph") names = append(names, "ceph")
names = append(names, "k3s") names = append(names, "k3s")
} }
@ -94,7 +94,7 @@ func (z *Zone) genHosts(out *hostsFile, p *Machine) {
if p.IsGateway() { if p.IsGateway() {
var s string var s string
ip, _ = p.RingZeroAddress() ip, _ = RingZeroAddress(p.zone.ID, p.ID)
s = fmt.Sprintf("%s-%v", p.Name, 0) s = fmt.Sprintf("%s-%v", p.Name, 0)
entry = hostsEntry{ entry = hostsEntry{

17
pkg/cluster/machine.go

@ -3,8 +3,6 @@ package cluster
import ( import (
"net/netip" "net/netip"
"strings" "strings"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
// revive:disable:line-length-limit // revive:disable:line-length-limit
@ -14,7 +12,7 @@ type Machine struct {
zone *Zone zone *Zone
logger `json:"-" yaml:"-"` logger `json:"-" yaml:"-"`
ID rings.NodeID ID int
Name string `json:"-" yaml:"-"` Name string `json:"-" yaml:"-"`
Inactive bool `json:"inactive,omitempty" yaml:"inactive,omitempty"` Inactive bool `json:"inactive,omitempty" yaml:"inactive,omitempty"`
@ -53,13 +51,13 @@ func (m *Machine) IsActive() bool {
// IsGateway tells if the Machine is a ring0 gateway // IsGateway tells if the Machine is a ring0 gateway
func (m *Machine) IsGateway() bool { func (m *Machine) IsGateway() bool {
_, ok := m.getRingInfo(rings.RingZeroID) _, ok := m.getRingInfo(0)
return ok return ok
} }
// SetGateway enables/disables a Machine ring0 integration // SetGateway enables/disables a Machine ring0 integration
func (m *Machine) SetGateway(enabled bool) error { func (m *Machine) SetGateway(enabled bool) error {
ri, found := m.getRingInfo(rings.RingZeroID) ri, found := m.getRingInfo(0)
switch { switch {
case !found && !enabled: case !found && !enabled:
return nil return nil
@ -72,19 +70,14 @@ func (m *Machine) SetGateway(enabled bool) error {
} }
ri.Enabled = enabled ri.Enabled = enabled
return m.SyncWireguardConfig(rings.RingZeroID) return m.SyncWireguardConfig(0)
} }
// Zone indicates the [Zone] this machine belongs to // Zone indicates the [Zone] this machine belongs to
func (m *Machine) Zone() rings.ZoneID { func (m *Machine) Zone() int {
return m.zone.ID return m.zone.ID
} }
// Region indicates the [Region] this machine belongs to
func (m *Machine) Region() rings.RegionID {
return m.zone.RegionID()
}
func (m *Machine) getPeerByName(name string) (*Machine, bool) { func (m *Machine) getPeerByName(name string) (*Machine, bool) {
return m.zone.zones.GetMachineByName(name) return m.zone.zones.GetMachineByName(name)
} }

124
pkg/cluster/machine_rings.go

@ -8,26 +8,18 @@ import (
"darvaza.org/core" "darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/rings"
"git.jpi.io/amery/jpictl/pkg/wireguard" "git.jpi.io/amery/jpictl/pkg/wireguard"
) )
// GetWireguardKeys reads a wgN.key/wgN.pub files // GetWireguardKeys reads a wgN.key/wgN.pub files
func (m *Machine) GetWireguardKeys(ringID rings.RingID) (wireguard.KeyPair, error) { func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) {
var ( var (
data []byte data []byte
err error
out wireguard.KeyPair out wireguard.KeyPair
) )
ring, err := AsWireguardInterfaceID(ringID) data, err = m.ReadFile("wg%v.key", ring)
if err != nil {
// invalid ring
return out, err
}
keyFile, pubFile, _ := ring.Files()
data, err = m.ReadFile(keyFile)
if err != nil { if err != nil {
// failed to read // failed to read
return out, err return out, err
@ -36,11 +28,11 @@ func (m *Machine) GetWireguardKeys(ringID rings.RingID) (wireguard.KeyPair, erro
out.PrivateKey, err = wireguard.PrivateKeyFromBase64(string(data)) out.PrivateKey, err = wireguard.PrivateKeyFromBase64(string(data))
if err != nil { if err != nil {
// bad key // bad key
err = core.Wrap(err, keyFile) err = core.Wrap(err, "wg%v.key", ring)
return out, err return out, err
} }
data, err = m.ReadFile(pubFile) data, err = m.ReadFile("wg%v.pub", ring)
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
// no wgN.pub is fine // no wgN.pub is fine
@ -52,7 +44,7 @@ func (m *Machine) GetWireguardKeys(ringID rings.RingID) (wireguard.KeyPair, erro
out.PublicKey, err = wireguard.PublicKeyFromBase64(string(data)) out.PublicKey, err = wireguard.PublicKeyFromBase64(string(data))
if err != nil { if err != nil {
// bad key // bad key
err = core.Wrap(err, pubFile) err = core.Wrap(err, "wg%v.pub", ring)
return out, err return out, err
} }
} }
@ -61,8 +53,8 @@ func (m *Machine) GetWireguardKeys(ringID rings.RingID) (wireguard.KeyPair, erro
return out, err return out, err
} }
func (m *Machine) tryReadWireguardKeys(ringID rings.RingID) error { func (m *Machine) tryReadWireguardKeys(ring int) error {
kp, err := m.GetWireguardKeys(ringID) kp, err := m.GetWireguardKeys(ring)
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
// ignore // ignore
@ -73,25 +65,20 @@ func (m *Machine) tryReadWireguardKeys(ringID rings.RingID) error {
default: default:
// import keys // import keys
ri := &RingInfo{ ri := &RingInfo{
Ring: MustWireguardInterfaceID(ringID), Ring: ring,
Keys: kp, Keys: kp,
} }
return m.applyRingInfo(ringID, ri) return m.applyRingInfo(ring, ri)
} }
} }
// RemoveWireguardKeys deletes wgN.key and wgN.pub from // RemoveWireguardKeys deletes wgN.key and wgN.pub from
// the machine's config directory // the machine's config directory
func (m *Machine) RemoveWireguardKeys(ringID rings.RingID) error { func (m *Machine) RemoveWireguardKeys(ring int) error {
ring, err := AsWireguardInterfaceID(ringID) var err error
if err != nil {
return err
}
keyFile, pubFile, _ := ring.Files()
err = m.RemoveFile(pubFile) err = m.RemoveFile("wg%v.pub", ring)
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
// ignore // ignore
@ -99,7 +86,7 @@ func (m *Machine) RemoveWireguardKeys(ringID rings.RingID) error {
return err return err
} }
err = m.RemoveFile(keyFile) err = m.RemoveFile("wg%v.key", ring)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// ignore // ignore
err = nil err = nil
@ -109,13 +96,8 @@ func (m *Machine) RemoveWireguardKeys(ringID rings.RingID) error {
} }
// GetWireguardConfig reads a wgN.conf file // GetWireguardConfig reads a wgN.conf file
func (m *Machine) GetWireguardConfig(ringID rings.RingID) (*wireguard.Config, error) { func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) {
ring, err := AsWireguardInterfaceID(ringID) data, err := m.ReadFile("wg%v.conf", ring)
if err != nil {
return nil, err
}
data, err := m.ReadFile(ring.ConfFile())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -124,7 +106,7 @@ func (m *Machine) GetWireguardConfig(ringID rings.RingID) (*wireguard.Config, er
return wireguard.NewConfigFromReader(r) return wireguard.NewConfigFromReader(r)
} }
func (m *Machine) tryApplyWireguardConfig(ring rings.RingID) error { func (m *Machine) tryApplyWireguardConfig(ring int) error {
wg, err := m.GetWireguardConfig(ring) wg, err := m.GetWireguardConfig(ring)
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
@ -136,15 +118,15 @@ func (m *Machine) tryApplyWireguardConfig(ring rings.RingID) error {
} }
} }
func (m *Machine) applyWireguardConfigNode(ring rings.RingID, wg *wireguard.Config) error { func (m *Machine) applyWireguardConfigNode(ring int, wg *wireguard.Config) error {
addr := wg.GetAddress() addr := wg.GetAddress()
if !core.IsZero(addr) { if !core.IsZero(addr) {
regionID, zoneID, nodeID, ok := Rings[ring].Decode(addr) zoneID, nodeID, ok := Rings[ring].Decode(addr)
if !ok { if !ok {
return fmt.Errorf("%s: invalid address", addr) return fmt.Errorf("%s: invalid address", addr)
} }
if err := m.applyZoneNodeID(regionID, zoneID, nodeID); err != nil { if err := m.applyZoneNodeID(zoneID, nodeID); err != nil {
return core.Wrap(err, "%s: invalid address", addr) return core.Wrap(err, "%s: invalid address", addr)
} }
} }
@ -156,7 +138,7 @@ func (m *Machine) applyWireguardConfigNode(ring rings.RingID, wg *wireguard.Conf
return nil return nil
} }
func (m *Machine) applyWireguardConfig(ring rings.RingID, wg *wireguard.Config) error { func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
if err := m.applyWireguardConfigNode(ring, wg); err != nil { if err := m.applyWireguardConfigNode(ring, wg); err != nil {
return err return err
} }
@ -170,7 +152,7 @@ func (m *Machine) applyWireguardConfig(ring rings.RingID, wg *wireguard.Config)
WithField("subsystem", "wireguard"). WithField("subsystem", "wireguard").
WithField("node", m.Name). WithField("node", m.Name).
WithField("peer", peer.Endpoint.Host). WithField("peer", peer.Endpoint.Host).
WithField("ring", MustWireguardInterfaceID(ring)). WithField("ring", ring).
Print("ignoring unknown endpoint") Print("ignoring unknown endpoint")
case err != nil: case err != nil:
return core.Wrap(err, "peer") return core.Wrap(err, "peer")
@ -180,9 +162,9 @@ func (m *Machine) applyWireguardConfig(ring rings.RingID, wg *wireguard.Config)
return nil return nil
} }
func (m *Machine) getRingInfo(ring rings.RingID) (*RingInfo, bool) { func (m *Machine) getRingInfo(ring int) (*RingInfo, bool) {
for _, ri := range m.Rings { for _, ri := range m.Rings {
if ri.RingID() == ring { if ri.Ring == ring {
return ri, ri.Enabled return ri, ri.Enabled
} }
} }
@ -190,13 +172,13 @@ func (m *Machine) getRingInfo(ring rings.RingID) (*RingInfo, bool) {
return nil, false return nil, false
} }
func (m *Machine) applyRingInfo(ring rings.RingID, new *RingInfo) error { func (m *Machine) applyRingInfo(ring int, new *RingInfo) error {
cur, _ := m.getRingInfo(ring) cur, _ := m.getRingInfo(ring)
if cur == nil { if cur == nil {
// first, append // first, append
m.debug(). m.debug().
WithField("node", m.Name). WithField("node", m.Name).
WithField("ring", MustWireguardInterfaceID(ring)). WithField("ring", ring).
Print("found") Print("found")
m.Rings = append(m.Rings, new) m.Rings = append(m.Rings, new)
return nil return nil
@ -206,11 +188,9 @@ func (m *Machine) applyRingInfo(ring rings.RingID, new *RingInfo) error {
return cur.Merge(new) return cur.Merge(new)
} }
func (m *Machine) applyWireguardInterfaceConfig(ring rings.RingID, func (m *Machine) applyWireguardInterfaceConfig(ring int, data wireguard.InterfaceConfig) error {
data wireguard.InterfaceConfig) error {
//
ri := &RingInfo{ ri := &RingInfo{
Ring: MustWireguardInterfaceID(ring), Ring: ring,
Enabled: true, Enabled: true,
Keys: wireguard.KeyPair{ Keys: wireguard.KeyPair{
PrivateKey: data.PrivateKey, PrivateKey: data.PrivateKey,
@ -220,9 +200,7 @@ func (m *Machine) applyWireguardInterfaceConfig(ring rings.RingID,
return m.applyRingInfo(ring, ri) return m.applyRingInfo(ring, ri)
} }
func (m *Machine) applyWireguardPeerConfig(ring rings.RingID, func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) error {
pc wireguard.PeerConfig) error {
//
peer, found := m.getPeerByName(pc.Endpoint.Name()) peer, found := m.getPeerByName(pc.Endpoint.Name())
switch { switch {
case !found: case !found:
@ -234,7 +212,7 @@ func (m *Machine) applyWireguardPeerConfig(ring rings.RingID,
default: default:
// apply RingInfo // apply RingInfo
ri := &RingInfo{ ri := &RingInfo{
Ring: MustWireguardInterfaceID(ring), Ring: ring,
Enabled: true, Enabled: true,
Keys: wireguard.KeyPair{ Keys: wireguard.KeyPair{
PublicKey: pc.PublicKey, PublicKey: pc.PublicKey,
@ -245,29 +223,21 @@ func (m *Machine) applyWireguardPeerConfig(ring rings.RingID,
} }
} }
func (m *Machine) applyZoneNodeID(regionID rings.RegionID, func (m *Machine) applyZoneNodeID(zoneID, nodeID int) error {
zoneID rings.ZoneID, nodeID rings.NodeID) error {
//
switch { switch {
case !regionID.Valid(): case zoneID == 0:
return fmt.Errorf("invalid %s", "regionID")
case !zoneID.Valid():
return fmt.Errorf("invalid %s", "zoneID") return fmt.Errorf("invalid %s", "zoneID")
case !nodeID.Valid(): case nodeID == 0:
return fmt.Errorf("invalid %s", "nodeID") return fmt.Errorf("invalid %s", "nodeID")
case m.ID != nodeID: case m.ID != nodeID:
return fmt.Errorf("invalid %s: %v ≠ %v", "nodeID", m.ID, nodeID) return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID, nodeID)
case m.zone.ID != 0 && m.zone.ID != zoneID: case m.zone.ID != 0 && m.zone.ID != zoneID:
return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID) return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID)
case m.Region() != regionID: case m.zone.ID == 0:
return fmt.Errorf("invalid %s: %v ≠ %v", "regionID", m.Region(), regionID) m.zone.ID = zoneID
default:
if m.zone.ID == 0 {
m.zone.ID = zoneID
}
return nil
} }
return nil
} }
func (m *Machine) setRingDefaults(ri *RingInfo) error { func (m *Machine) setRingDefaults(ri *RingInfo) error {
@ -289,13 +259,8 @@ func (m *Machine) setRingDefaults(ri *RingInfo) error {
// RemoveWireguardConfig deletes wgN.conf from the machine's // RemoveWireguardConfig deletes wgN.conf from the machine's
// config directory. // config directory.
func (m *Machine) RemoveWireguardConfig(ringID rings.RingID) error { func (m *Machine) RemoveWireguardConfig(ring int) error {
ring, err := AsWireguardInterfaceID(ringID) err := m.RemoveFile("wg%v.conf", ring)
if err != nil {
return err
}
err = m.RemoveFile(ring.ConfFile())
if os.IsNotExist(err) { if os.IsNotExist(err) {
err = nil err = nil
} }
@ -303,12 +268,7 @@ func (m *Machine) RemoveWireguardConfig(ringID rings.RingID) error {
return err return err
} }
func (m *Machine) createRingInfo(ringID rings.RingID, enabled bool) (*RingInfo, error) { func (m *Machine) createRingInfo(ring int, enabled bool) (*RingInfo, error) {
ring, err := AsWireguardInterfaceID(ringID)
if err != nil {
return nil, err
}
keys, err := wireguard.NewKeyPair() keys, err := wireguard.NewKeyPair()
if err != nil { if err != nil {
return nil, err return nil, err
@ -320,7 +280,7 @@ func (m *Machine) createRingInfo(ringID rings.RingID, enabled bool) (*RingInfo,
Keys: keys, Keys: keys,
} }
err = m.applyRingInfo(ringID, ri) err = m.applyRingInfo(ring, ri)
if err != nil { if err != nil {
return nil, err return nil, err
} }

14
pkg/cluster/machine_scan.go

@ -9,8 +9,6 @@ import (
"time" "time"
"darvaza.org/core" "darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
// LookupNetIP uses the DNS Resolver to get the public addresses associated // LookupNetIP uses the DNS Resolver to get the public addresses associated
@ -39,8 +37,8 @@ func (m *Machine) init() error {
return core.Wrap(err, m.Name) return core.Wrap(err, m.Name)
} }
for _, ring := range Rings { for i := 0; i < RingsCount; i++ {
if err := m.tryReadWireguardKeys(ring.ID); err != nil { if err := m.tryReadWireguardKeys(i); err != nil {
return core.Wrap(err, m.Name) return core.Wrap(err, m.Name)
} }
} }
@ -67,18 +65,18 @@ func (m *Machine) setID() error {
return err return err
} }
m.ID = rings.NodeID(id) m.ID = int(id)
return nil return nil
} }
// scan is called once we know about all zones and machine names // scan is called once we know about all zones and machine names
func (m *Machine) scan(_ *ScanOptions) error { func (m *Machine) scan(_ *ScanOptions) error {
for _, ring := range Rings { for i := 0; i < RingsCount; i++ {
if err := m.tryApplyWireguardConfig(ring.ID); err != nil { if err := m.tryApplyWireguardConfig(i); err != nil {
m.error(err). m.error(err).
WithField("subsystem", "wireguard"). WithField("subsystem", "wireguard").
WithField("node", m.Name). WithField("node", m.Name).
WithField("ring", MustWireguardInterfaceID(ring.ID)). WithField("ring", i).
Print() Print()
return err return err
} }

232
pkg/cluster/regions.go

@ -3,38 +3,20 @@ package cluster
import ( import (
"bytes" "bytes"
"path/filepath" "path/filepath"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
var ( var (
_ MachineIterator = (*Region)(nil) _ MachineIterator = (*Region)(nil)
_ ZoneIterator = (*Region)(nil) _ ZoneIterator = (*Region)(nil)
_ RegionIterator = (*Zone)(nil)
_ RegionIterator = (*Cluster)(nil)
) )
// A RegionIterator is a set of Regions we can iterate on
type RegionIterator interface {
ForEachRegion(func(*Region) bool)
}
// Region represents a group of zones geographically related // Region represents a group of zones geographically related
type Region struct { type Region struct {
m *Cluster m *Cluster
zones []*Zone zones []*Zone
Name string Name string
ID rings.RegionID `json:",omitempty" yaml:",omitempty"` Regions []string `json:",omitempty" yaml:",omitempty"`
Cluster *string `json:",omitempty" yaml:",omitempty"`
Regions []string `json:",omitempty" yaml:",omitempty"`
}
// IsPrimary indicates the region is primary and corresponds
// to a kubernetes cluster.
func (r *Region) IsPrimary() bool {
return r != nil && r.Cluster != nil
} }
// ForEachRegion calls a function for each Region of the cluster // ForEachRegion calls a function for each Region of the cluster
@ -101,8 +83,6 @@ func (m *Cluster) initRegions(_ *ScanOptions) error {
} }
m.sortRegions() m.sortRegions()
m.scanRegionID()
m.computeZonesRegion()
return nil return nil
} }
@ -126,26 +106,6 @@ func (m *Cluster) setRegionZones(name string, zones ...*Zone) {
}) })
} }
func (m *Cluster) setRegionClusterToken(name string, token string) error {
for i := range m.Regions {
r := &m.Regions[i]
if r.Name == name {
// found
r.Cluster = &token
return nil
}
}
// new
m.Regions = append(m.Regions, Region{
m: m,
Name: name,
Cluster: &token,
})
return nil
}
func (m *Cluster) appendRegionRegions(name string, subs ...string) { func (m *Cluster) appendRegionRegions(name string, subs ...string) {
for i := range m.Regions { for i := range m.Regions {
r := &m.Regions[i] r := &m.Regions[i]
@ -164,28 +124,6 @@ func (m *Cluster) appendRegionRegions(name string, subs ...string) {
}) })
} }
// ForEachRegion calls a function on all regions this zone belongs to.
func (z *Zone) ForEachRegion(fn func(*Region) bool) {
if fn == nil {
return
}
z.zones.ForEachRegion(func(r *Region) bool {
var match bool
r.ForEachZone(func(z2 *Zone) bool {
match = (z == z2)
return match
})
if match && fn(r) {
return true
}
return false
})
}
func (z *Zone) appendRegions(regions ...string) error { func (z *Zone) appendRegions(regions ...string) error {
for _, s := range regions { for _, s := range regions {
// TODO: validate // TODO: validate
@ -209,7 +147,7 @@ func (m *Cluster) finishRegion(r *Region) {
r.m = m r.m = m
sub := []string{} sub := []string{}
for _, name := range r.Regions { for _, name := range r.Regions {
r2, ok := m.getFinishRegion(name) r2, ok := m.getRegion(name)
if !ok { if !ok {
m.warn(nil).WithField("region", name).Print("unknown region") m.warn(nil).WithField("region", name).Print("unknown region")
continue continue
@ -221,96 +159,12 @@ func (m *Cluster) finishRegion(r *Region) {
r.Regions = sub r.Regions = sub
} }
// revive:disable:cognitive-complexity
func (m *Cluster) scanRegionID() {
// revive:enable:cognitive-complexity
var max rings.RegionID
var missing bool
// check IDs
ids := make(map[rings.RegionID]bool)
fn := func(r *Region) bool {
var term bool
switch {
case !r.IsPrimary():
// secondary, no ID.
r.ID = 0
case !r.ID.Valid():
// primary without ID
missing = true
case ids[r.ID]:
// duplicate
m.error(nil).WithField("region", r.Name).Print("duplicate ID")
missing = true
r.ID = 0
default:
ids[r.ID] = true
if r.ID > max {
max = r.ID
}
}
return term
}
m.ForEachRegion(fn)
if missing {
// assign missing IDs
fn := func(r *Region) bool {
var term bool
switch {
case !r.IsPrimary():
// ignore secondary
case r.ID.Valid():
// already has an ID
default:
r.ID = max + 1
max = r.ID
}
return term
}
m.ForEachRegion(fn)
}
}
func (m *Cluster) computeZonesRegion() {
fn := func(r *Region, z *Zone) {
if z.region != nil {
m.error(nil).
WithField("zone", z.Name).
WithField("region", []string{
z.region.Name,
r.Name,
}).
Print("zone in two regions")
} else {
z.region = r
}
}
m.ForEachRegion(func(r *Region) bool {
var term bool
if r.IsPrimary() {
r.ForEachZone(func(z *Zone) bool {
fn(r, z)
return term
})
}
return term
})
}
func (m *Cluster) getRegion(name string) (*Region, bool) { func (m *Cluster) getRegion(name string) (*Region, bool) {
for i := range m.Regions { for i := range m.Regions {
r := &m.Regions[i] r := &m.Regions[i]
if name == r.Name { if name == r.Name {
m.finishRegion(r)
return r, true return r, true
} }
} }
@ -318,22 +172,17 @@ func (m *Cluster) getRegion(name string) (*Region, bool) {
return nil, false return nil, false
} }
func (m *Cluster) getFinishRegion(name string) (*Region, bool) {
if r, ok := m.getRegion(name); ok {
m.finishRegion(r)
return r, true
}
return nil, false
}
// SyncRegions writes to the file system the regions this [Zone] // SyncRegions writes to the file system the regions this [Zone]
// belongs to. // belongs to.
func (z *Zone) SyncRegions() error { func (z *Zone) SyncRegions() error {
err := z.syncZoneRegions() err := z.syncZoneRegions()
if err == nil { if err == nil {
z.ForEachMachine(func(p *Machine) bool { z.ForEachMachine(func(p *Machine) bool {
err = z.syncMachineRegions(p) if p.IsActive() {
err = p.RemoveFile("region")
} else {
err = p.WriteStringFile("none\n", "region")
}
return err != nil return err != nil
}) })
} }
@ -341,24 +190,8 @@ func (z *Zone) SyncRegions() error {
return err return err
} }
func (*Zone) syncMachineRegions(p *Machine) error {
var err error
if p.IsActive() {
err = p.RemoveFile("region")
} else {
err = p.WriteStringFile("none\n", "region")
}
if err == nil {
err = p.RemoveFile(RegionClusterTokenFileName)
}
return err
}
func (z *Zone) syncZoneRegions() error { func (z *Zone) syncZoneRegions() error {
name := filepath.Join(z.Name, ZoneRegionsFileName) name := filepath.Join(z.Name, "regions")
if len(z.Regions) > 0 { if len(z.Regions) > 0 {
var buf bytes.Buffer var buf bytes.Buffer
@ -377,25 +210,9 @@ func (z *Zone) syncZoneRegions() error {
// SyncRegions writes to the file system the regions covered // SyncRegions writes to the file system the regions covered
// by this meta-region // by this meta-region
func (r *Region) SyncRegions() error { func (r *Region) SyncRegions() error {
if err := r.syncRegionsFile(); err != nil { name := filepath.Join(r.Name, "regions")
return err
}
return r.syncClusterFile()
}
func (r *Region) mkdir() error {
return r.m.MkdirAll(r.Name)
}
func (r *Region) syncRegionsFile() error {
var err error
name := filepath.Join(r.Name, ZoneRegionsFileName) if len(r.Regions) > 0 {
if len(r.Regions) == 0 {
err = r.m.RemoveFile(name)
} else if err = r.mkdir(); err == nil {
var buf bytes.Buffer var buf bytes.Buffer
for _, s := range r.Regions { for _, s := range r.Regions {
@ -403,29 +220,12 @@ func (r *Region) syncRegionsFile() error {
_, _ = buf.WriteRune('\n') _, _ = buf.WriteRune('\n')
} }
err = r.m.WriteStringFile(buf.String(), name) if err := r.m.MkdirAll(r.Name); err != nil {
} return err
return err
}
func (r *Region) syncClusterFile() error {
var err error
name := filepath.Join(r.Name, RegionClusterTokenFileName)
if r.Cluster == nil {
err = r.m.RemoveFile(name)
} else if err = r.mkdir(); err == nil {
var buf bytes.Buffer
_, _ = buf.WriteString(*r.Cluster)
if buf.Len() > 0 {
_, _ = buf.WriteRune('\n')
} }
err = r.m.WriteStringFile(buf.String(), name) return r.m.WriteStringFile(buf.String(), name)
} }
return err return r.m.RemoveFile(name)
} }

250
pkg/cluster/rings.go

@ -4,84 +4,31 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net/netip" "net/netip"
"strconv"
"git.jpi.io/amery/jpictl/pkg/rings"
"git.jpi.io/amery/jpictl/pkg/wireguard" "git.jpi.io/amery/jpictl/pkg/wireguard"
) )
const ( const (
// MaxZoneID indicates the highest ID allowed for a Zone
MaxZoneID = 0xf
// MaxNodeID indicates the highest Machine ID allowed within a Zone
MaxNodeID = 0xff - 1
// RingsCount indicates how many wireguard rings we have
RingsCount = 2
// RingZeroPort is the port wireguard uses for ring0 // RingZeroPort is the port wireguard uses for ring0
RingZeroPort = 51800 RingZeroPort = 51800
// RingOnePort is the port wireguard uses for ring1 // RingOnePort is the port wireguard uses for ring1
RingOnePort = 51810 RingOnePort = 51810
) )
// WireguardInterfaceID represents the number in the `wg%v`
// interface name.
type WireguardInterfaceID uint
// AsWireguardInterfaceID returns the [WireguardInterfaceID] for
// a valid [rings.RingID].
func AsWireguardInterfaceID(ring rings.RingID) (WireguardInterfaceID, error) {
switch ring {
case rings.RingZeroID:
return 0, nil
default:
return 0, ErrInvalidRing(ring)
}
}
// MustWireguardInterfaceID returns the [WireguardInterfaceID] for
// a valid [rings.RingID], and panics if it's not.
func MustWireguardInterfaceID(ring rings.RingID) WireguardInterfaceID {
id, err := AsWireguardInterfaceID(ring)
if err != nil {
panic(err)
}
return id
}
// RingID tells the [rings.RingID] of the [WireguardInterfaceID].
func (wi WireguardInterfaceID) RingID() rings.RingID {
return rings.RingID(wi + 1)
}
// PubFile returns "wgN.pub"
func (wi WireguardInterfaceID) PubFile() string {
return fmt.Sprintf("wg%v.pub", wi)
}
// KeyFile returns "wgN.key"
func (wi WireguardInterfaceID) KeyFile() string {
return fmt.Sprintf("wg%v.key", wi)
}
// ConfFile returns "wgN.conf"
func (wi WireguardInterfaceID) ConfFile() string {
return fmt.Sprintf("wg%v.conf", wi)
}
// Files returns all wgN.ext file names.
func (wi WireguardInterfaceID) Files() (keyFile, pubFile, confFile string) {
prefix := "wg" + strconv.Itoa(int(wi))
return prefix + ".key", prefix + ".pub", prefix + ".conf"
}
// RingInfo contains represents the Wireguard endpoint details // RingInfo contains represents the Wireguard endpoint details
// for a Machine on a particular ring // for a Machine on a particular ring
type RingInfo struct { type RingInfo struct {
Ring WireguardInterfaceID Ring int
Enabled bool Enabled bool
Keys wireguard.KeyPair Keys wireguard.KeyPair
} }
// RingID returns the [rings.RingID] for this [RingInfo].
func (ri *RingInfo) RingID() rings.RingID {
return rings.RingID(ri.Ring + 1)
}
// Merge attempts to combine two RingInfo structs // Merge attempts to combine two RingInfo structs
func (ri *RingInfo) Merge(alter *RingInfo) error { func (ri *RingInfo) Merge(alter *RingInfo) error {
switch { switch {
@ -107,7 +54,7 @@ func (ri *RingInfo) unsafeMerge(alter *RingInfo) error {
ri.Enabled = true ri.Enabled = true
} }
// fill the gaps on our key pair // fill the gaps on our keypair
if ri.Keys.PrivateKey.IsZero() { if ri.Keys.PrivateKey.IsZero() {
ri.Keys.PrivateKey = alter.Keys.PrivateKey ri.Keys.PrivateKey = alter.Keys.PrivateKey
} }
@ -132,26 +79,108 @@ func canMergeKeyPairs(p1, p2 wireguard.KeyPair) bool {
// RingAddressEncoder provides encoder/decoder access for a particular // RingAddressEncoder provides encoder/decoder access for a particular
// Wireguard ring // Wireguard ring
type RingAddressEncoder struct { type RingAddressEncoder struct {
ID rings.RingID ID int
Port uint16 Port uint16
Encode func(rings.RegionID, rings.ZoneID, rings.NodeID) (netip.Addr, error) Encode func(zoneID, nodeID int) (netip.Addr, bool)
Decode func(addr netip.Addr) (rings.RegionID, rings.ZoneID, rings.NodeID, bool) Decode func(addr netip.Addr) (zoneID, nodeID int, ok bool)
} }
var ( var (
// RingZero is a wg0 address encoder/decoder // RingZero is a wg0 address encoder/decoder
RingZero = RingAddressEncoder{ RingZero = RingAddressEncoder{
ID: rings.RingZeroID, ID: 0,
Port: RingZeroPort, Port: RingZeroPort,
Decode: rings.DecodeRingZeroAddress, Decode: ParseRingZeroAddress,
Encode: rings.RingZeroAddress, Encode: RingZeroAddress,
}
// RingOne is a wg1 address encoder/decoder
RingOne = RingAddressEncoder{
ID: 1,
Port: RingOnePort,
Decode: ParseRingOneAddress,
Encode: RingOneAddress,
} }
// Rings provides indexed access to the ring address encoders // Rings provides indexed access to the ring address encoders
Rings = []RingAddressEncoder{ Rings = [RingsCount]RingAddressEncoder{
RingZero, RingZero,
RingOne,
} }
) )
// ValidZoneID checks if the given zoneID is a valid 4 bit zone number.
//
// 0 is reserved, and only allowed when composing CIDRs.
func ValidZoneID(zoneID int) bool {
switch {
case zoneID < 0 || zoneID > MaxZoneID:
return false
default:
return true
}
}
// ValidNodeID checks if the given nodeID is a valid 8 bit number.
// nodeID is unique within a Zone.
// 0 is reserved, and only allowed when composing CIDRs.
func ValidNodeID(nodeID int) bool {
switch {
case nodeID < 0 || nodeID > MaxNodeID:
return false
default:
return true
}
}
// ParseRingZeroAddress extracts zone and node ID from a wg0 [netip.Addr]
// wg0 addresses are of the form `10.0.{{zoneID}}.{{nodeID}}`
func ParseRingZeroAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[1] == 0 {
return int(a4[2]), int(a4[3]), true
}
}
return 0, 0, false
}
// RingZeroAddress returns a wg0 IP address
func RingZeroAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
default:
a4 := [4]uint8{10, 0, uint8(zoneID), uint8(nodeID)}
return netip.AddrFrom4(a4), true
}
}
// ParseRingOneAddress extracts zone and node ID from a wg1 [netip.Addr]
// wg1 addresses are of the form `10.{{zoneID << 4}}.{{nodeID}}`
func ParseRingOneAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[2] == 0 {
zoneID = int(a4[1] >> 4)
nodeID = int(a4[3])
return zoneID, nodeID, true
}
}
return 0, 0, false
}
// RingOneAddress returns a wg1 IP address
func RingOneAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
default:
a4 := [4]uint8{10, uint8(zoneID << 4), 0, uint8(nodeID)}
return netip.AddrFrom4(a4), true
}
}
var ( var (
_ MachineIterator = (*Ring)(nil) _ MachineIterator = (*Ring)(nil)
_ ZoneIterator = (*Ring)(nil) _ ZoneIterator = (*Ring)(nil)
@ -174,15 +203,14 @@ func (r *Ring) AddPeer(p *Machine) bool {
nodeID := p.ID nodeID := p.ID
zoneID := p.Zone() zoneID := p.Zone()
regionID := p.Region() addr, _ := r.Encode(zoneID, nodeID)
addr, _ := r.Encode(regionID, zoneID, nodeID)
rp := &RingPeer{ rp := &RingPeer{
Node: p, Node: p,
Address: addr, Address: addr,
PrivateKey: ri.Keys.PrivateKey, PrivateKey: ri.Keys.PrivateKey,
PeerConfig: wireguard.PeerConfig{ PeerConfig: wireguard.PeerConfig{
Name: fmt.Sprintf("%s-%v", p.Name, ri.Ring), Name: fmt.Sprintf("%s-%v", p.Name, r.ID),
PublicKey: ri.Keys.PublicKey, PublicKey: ri.Keys.PublicKey,
Endpoint: wireguard.EndpointAddress{ Endpoint: wireguard.EndpointAddress{
Host: p.FullName(), Host: p.FullName(),
@ -191,17 +219,61 @@ func (r *Ring) AddPeer(p *Machine) bool {
}, },
} }
r.setRingZeroAllowedIPs(rp) switch {
case r.ID == 0:
r.setRingZeroAllowedIPs(rp)
case p.IsGateway():
r.setRingOneGatewayAllowedIPs(rp)
default:
r.setRingOneNodeAllowedIPs(rp)
}
r.Peers = append(r.Peers, rp) r.Peers = append(r.Peers, rp)
return true return true
} }
func (*Ring) setRingZeroAllowedIPs(rp *RingPeer) { func (r *Ring) setRingZeroAllowedIPs(rp *RingPeer) {
// ring0 peer zoneID, _, _ := r.Decode(rp.Address)
// everyone on ring0 is a gateway to ring1
addr, _ := RingOneAddress(zoneID, 0)
rp.AllowCIDR(addr, 12)
// peer
rp.AllowCIDR(rp.Address, 32) rp.AllowCIDR(rp.Address, 32)
}
func (r *Ring) setRingOneGatewayAllowedIPs(rp *RingPeer) {
zoneID, _, _ := r.Decode(rp.Address)
// peer
rp.AllowCIDR(rp.Address, 32)
// ring1 gateways connect to all other ring1 networks
r.ForEachZone(func(z *Zone) bool {
if z.ID != zoneID {
addr, _ := r.Encode(z.ID, 0)
rp.AllowCIDR(addr, 12)
}
return false
})
// ring1 gateways also connect to all ring0 addresses
r.ForEachZone(func(z *Zone) bool {
z.ForEachMachine(func(p *Machine) bool {
if p.IsGateway() {
addr, _ := RingZeroAddress(z.ID, p.ID)
rp.AllowCIDR(addr, 32)
}
return false
})
return false
})
}
// everyone on ring0 has a leg on ring1 func (*Ring) setRingOneNodeAllowedIPs(rp *RingPeer) {
rp.AllowCIDR(rp.Node.RingOneAddress(), 32) // only to the peer itself
rp.AllowCIDR(rp.Address, 32)
} }
// ForEachMachine calls a function for each Machine in the ring // ForEachMachine calls a function for each Machine in the ring
@ -257,29 +329,15 @@ type RingPeer struct {
// AllowCIDR allows an IP range via this peer // AllowCIDR allows an IP range via this peer
func (rp *RingPeer) AllowCIDR(addr netip.Addr, bits int) { func (rp *RingPeer) AllowCIDR(addr netip.Addr, bits int) {
rp.AllowSubnet(netip.PrefixFrom(addr, bits)) cidr := netip.PrefixFrom(addr, bits)
} rp.PeerConfig.AllowedIPs = append(rp.PeerConfig.AllowedIPs, cidr)
// AllowSubnet allows an IP range via this peer
func (rp *RingPeer) AllowSubnet(subnet netip.Prefix) {
rp.PeerConfig.AllowedIPs = append(rp.PeerConfig.AllowedIPs, subnet)
} }
// NewRing composes a new Ring for Wireguard setup // NewRing composes a new Ring for Wireguard setup
func NewRing(z ZoneIterator, m MachineIterator, ringID rings.RingID) (*Ring, error) { func NewRing(z ZoneIterator, m MachineIterator, ring int) (*Ring, error) {
var r *Ring r := &Ring{
for _, ring := range Rings { RingAddressEncoder: Rings[ring],
if ringID == ring.ID { ZoneIterator: z,
r = &Ring{
RingAddressEncoder: ring,
ZoneIterator: z,
}
break
}
}
if r == nil {
return nil, ErrInvalidRing(ringID)
} }
m.ForEachMachine(func(p *Machine) bool { m.ForEachMachine(func(p *Machine) bool {

6
pkg/cluster/sync.go

@ -35,13 +35,13 @@ func (m *Cluster) SyncMkdirAll() error {
func (m *Cluster) SyncAllWireguard() error { func (m *Cluster) SyncAllWireguard() error {
var err error var err error
for _, ring := range Rings { for ring := 0; ring < RingsCount; ring++ {
err = m.WriteWireguardKeys(ring.ID) err = m.WriteWireguardKeys(ring)
if err != nil { if err != nil {
return err return err
} }
err = m.SyncWireguardConfig(ring.ID) err = m.SyncWireguardConfig(ring)
if err != nil { if err != nil {
return err return err
} }

90
pkg/cluster/wireguard.go

@ -3,8 +3,6 @@ package cluster
import ( import (
"io/fs" "io/fs"
"os" "os"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
var ( var (
@ -28,22 +26,22 @@ var (
// A WireguardConfigPruner deletes wgN.conf on all machines under // A WireguardConfigPruner deletes wgN.conf on all machines under
// its scope with the specified ring disabled // its scope with the specified ring disabled
type WireguardConfigPruner interface { type WireguardConfigPruner interface {
PruneWireguardConfig(ring rings.RingID) error PruneWireguardConfig(ring int) error
} }
// PruneWireguardConfig removes wgN.conf files of machines with // PruneWireguardConfig removes wgN.conf files of machines with
// the corresponding ring disabled on all zones // the corresponding ring disabled on all zones
func (m *Cluster) PruneWireguardConfig(ring rings.RingID) error { func (m *Cluster) PruneWireguardConfig(ring int) error {
return pruneWireguardConfig(m, ring) return pruneWireguardConfig(m, ring)
} }
// PruneWireguardConfig removes wgN.conf files of machines with // PruneWireguardConfig removes wgN.conf files of machines with
// the corresponding ring disabled. // the corresponding ring disabled.
func (z *Zone) PruneWireguardConfig(ring rings.RingID) error { func (z *Zone) PruneWireguardConfig(ring int) error {
return pruneWireguardConfig(z, ring) return pruneWireguardConfig(z, ring)
} }
func pruneWireguardConfig(m MachineIterator, ring rings.RingID) error { func pruneWireguardConfig(m MachineIterator, ring int) error {
var err error var err error
m.ForEachMachine(func(p *Machine) bool { m.ForEachMachine(func(p *Machine) bool {
@ -61,7 +59,7 @@ func pruneWireguardConfig(m MachineIterator, ring rings.RingID) error {
// PruneWireguardConfig deletes the wgN.conf file if its // PruneWireguardConfig deletes the wgN.conf file if its
// presence on the ring is disabled // presence on the ring is disabled
func (m *Machine) PruneWireguardConfig(ring rings.RingID) error { func (m *Machine) PruneWireguardConfig(ring int) error {
_, ok := m.getRingInfo(ring) _, ok := m.getRingInfo(ring)
if !ok { if !ok {
return m.RemoveWireguardConfig(ring) return m.RemoveWireguardConfig(ring)
@ -73,32 +71,41 @@ func (m *Machine) PruneWireguardConfig(ring rings.RingID) error {
// A WireguardConfigWriter rewrites all wgN.conf on all machines under // A WireguardConfigWriter rewrites all wgN.conf on all machines under
// its scope attached to that ring // its scope attached to that ring
type WireguardConfigWriter interface { type WireguardConfigWriter interface {
WriteWireguardConfig(ring rings.RingID) error WriteWireguardConfig(ring int) error
} }
// WriteWireguardConfig rewrites all wgN.conf on all machines // WriteWireguardConfig rewrites all wgN.conf on all machines
// attached to that ring // attached to that ring
func (m *Cluster) WriteWireguardConfig(ring rings.RingID) error { func (m *Cluster) WriteWireguardConfig(ring int) error {
switch ring { switch ring {
case rings.RingZeroID: case 0:
return writeWireguardConfig(m, m, ring) return writeWireguardConfig(m, m, ring)
case 1:
var err error
m.ForEachZone(func(z *Zone) bool {
err = writeWireguardConfig(m, z, ring)
return err != nil
})
return err
default: default:
return ErrInvalidRing(ring) return fs.ErrInvalid
} }
} }
// WriteWireguardConfig rewrites all wgN.conf on all machines // WriteWireguardConfig rewrites all wgN.conf on all machines
// on the Zone attached to that ring // on the Zone attached to that ring
func (z *Zone) WriteWireguardConfig(ring rings.RingID) error { func (z *Zone) WriteWireguardConfig(ring int) error {
switch ring { switch ring {
case rings.RingZeroID: case 0:
return writeWireguardConfig(z.zones, z.zones, ring) return writeWireguardConfig(z.zones, z.zones, ring)
case 1:
return writeWireguardConfig(z.zones, z, ring)
default: default:
return ErrInvalidRing(ring) return fs.ErrInvalid
} }
} }
func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID) error { func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error {
r, err := NewRing(z, m, ring) r, err := NewRing(z, m, ring)
if err != nil { if err != nil {
return err return err
@ -114,7 +121,7 @@ func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID)
// WriteWireguardConfig rewrites the wgN.conf file of this Machine // WriteWireguardConfig rewrites the wgN.conf file of this Machine
// if enabled // if enabled
func (m *Machine) WriteWireguardConfig(ring rings.RingID) error { func (m *Machine) WriteWireguardConfig(ring int) error {
r, err := NewRing(m.zone.zones, m.zone, ring) r, err := NewRing(m.zone.zones, m.zone, ring)
if err != nil { if err != nil {
return err return err
@ -124,17 +131,12 @@ func (m *Machine) WriteWireguardConfig(ring rings.RingID) error {
} }
func (m *Machine) writeWireguardRingConfig(r *Ring) error { func (m *Machine) writeWireguardRingConfig(r *Ring) error {
ring, err := AsWireguardInterfaceID(r.ID)
if err != nil {
return err
}
wg, err := r.ExportConfig(m) wg, err := r.ExportConfig(m)
if err != nil { if err != nil {
return nil return nil
} }
f, err := m.CreateTruncFile(ring.ConfFile()) f, err := m.CreateTruncFile("wg%v.conf", r.ID)
if err != nil { if err != nil {
return err return err
} }
@ -147,32 +149,41 @@ func (m *Machine) writeWireguardRingConfig(r *Ring) error {
// A WireguardConfigSyncer updates all wgN.conf on all machines under // A WireguardConfigSyncer updates all wgN.conf on all machines under
// its scope reflecting the state of the ring // its scope reflecting the state of the ring
type WireguardConfigSyncer interface { type WireguardConfigSyncer interface {
SyncWireguardConfig(ring rings.RingID) error SyncWireguardConfig(ring int) error
} }
// SyncWireguardConfig updates all wgN.conf files for the specified // SyncWireguardConfig updates all wgN.conf files for the specified
// ring // ring
func (m *Cluster) SyncWireguardConfig(ring rings.RingID) error { func (m *Cluster) SyncWireguardConfig(ring int) error {
switch ring { switch ring {
case rings.RingZeroID: case 0:
return syncWireguardConfig(m, m, ring) return syncWireguardConfig(m, m, ring)
case 1:
var err error
m.ForEachZone(func(z *Zone) bool {
err = syncWireguardConfig(m, z, ring)
return err != nil
})
return err
default: default:
return ErrInvalidRing(ring) return fs.ErrInvalid
} }
} }
// SyncWireguardConfig updates all wgN.conf files for the specified // SyncWireguardConfig updates all wgN.conf files for the specified
// ring // ring
func (z *Zone) SyncWireguardConfig(ring rings.RingID) error { func (z *Zone) SyncWireguardConfig(ring int) error {
switch ring { switch ring {
case rings.RingZeroID: case 0:
return syncWireguardConfig(z.zones, z.zones, ring) return syncWireguardConfig(z.zones, z.zones, ring)
case 1:
return syncWireguardConfig(z.zones, z, ring)
default: default:
return ErrInvalidRing(ring) return fs.ErrInvalid
} }
} }
func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID) error { func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error {
r, err := NewRing(z, m, ring) r, err := NewRing(z, m, ring)
if err != nil { if err != nil {
return err return err
@ -192,27 +203,27 @@ func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID) e
// SyncWireguardConfig updates all wgN.conf files for the specified // SyncWireguardConfig updates all wgN.conf files for the specified
// ring // ring
func (m *Machine) SyncWireguardConfig(ring rings.RingID) error { func (m *Machine) SyncWireguardConfig(ring int) error {
return m.zone.SyncWireguardConfig(ring) return m.zone.SyncWireguardConfig(ring)
} }
// A WireguardKeysWriter writes the Wireguard Keys for all machines // A WireguardKeysWriter writes the Wireguard Keys for all machines
// under its scope for the specified ring // under its scope for the specified ring
type WireguardKeysWriter interface { type WireguardKeysWriter interface {
WriteWireguardKeys(ring rings.RingID) error WriteWireguardKeys(ring int) error
} }
// WriteWireguardKeys rewrites all wgN.{key,pub} files // WriteWireguardKeys rewrites all wgN.{key,pub} files
func (m *Cluster) WriteWireguardKeys(ring rings.RingID) error { func (m *Cluster) WriteWireguardKeys(ring int) error {
return writeWireguardKeys(m, ring) return writeWireguardKeys(m, ring)
} }
// WriteWireguardKeys rewrites all wgN.{key,pub} files on this zone // WriteWireguardKeys rewrites all wgN.{key,pub} files on this zone
func (z *Zone) WriteWireguardKeys(ring rings.RingID) error { func (z *Zone) WriteWireguardKeys(ring int) error {
return writeWireguardKeys(z, ring) return writeWireguardKeys(z, ring)
} }
func writeWireguardKeys(m MachineIterator, ring rings.RingID) error { func writeWireguardKeys(m MachineIterator, ring int) error {
var err error var err error
m.ForEachMachine(func(p *Machine) bool { m.ForEachMachine(func(p *Machine) bool {
@ -229,12 +240,12 @@ func writeWireguardKeys(m MachineIterator, ring rings.RingID) error {
} }
// WriteWireguardKeys writes the wgN.key/wgN.pub files // WriteWireguardKeys writes the wgN.key/wgN.pub files
func (m *Machine) WriteWireguardKeys(ringID rings.RingID) error { func (m *Machine) WriteWireguardKeys(ring int) error {
var err error var err error
var key, pub string var key, pub string
var ri *RingInfo var ri *RingInfo
ri, _ = m.getRingInfo(ringID) ri, _ = m.getRingInfo(ring)
if ri != nil { if ri != nil {
key = ri.Keys.PrivateKey.String() key = ri.Keys.PrivateKey.String()
pub = ri.Keys.PublicKey.String() pub = ri.Keys.PublicKey.String()
@ -247,13 +258,12 @@ func (m *Machine) WriteWireguardKeys(ringID rings.RingID) error {
pub = ri.Keys.PrivateKey.Public().String() pub = ri.Keys.PrivateKey.Public().String()
} }
keyFile, pubFile, _ := ri.Ring.Files() err = m.WriteStringFile(key+"\n", "wg%v.key", ring)
err = m.WriteStringFile(key+"\n", keyFile)
if err != nil { if err != nil {
return err return err
} }
err = m.WriteStringFile(pub+"\n", pubFile) err = m.WriteStringFile(pub+"\n", "wg%v.pub", ring)
if err != nil { if err != nil {
return err return err
} }

46
pkg/cluster/zones.go

@ -2,8 +2,6 @@ package cluster
import ( import (
"io/fs" "io/fs"
"git.jpi.io/amery/jpictl/pkg/rings"
) )
var ( var (
@ -19,10 +17,9 @@ type ZoneIterator interface {
// affinity. // affinity.
type Zone struct { type Zone struct {
zones *Cluster zones *Cluster
region *Region
logger `json:"-" yaml:"-"` logger `json:"-" yaml:"-"`
ID rings.ZoneID ID int
Name string Name string
Regions []string `json:",omitempty" yaml:",omitempty"` Regions []string `json:",omitempty" yaml:",omitempty"`
@ -34,7 +31,7 @@ func (z *Zone) String() string {
} }
// SetGateway configures a machine to be the zone's ring0 gateway // SetGateway configures a machine to be the zone's ring0 gateway
func (z *Zone) SetGateway(gatewayID rings.NodeID, enabled bool) error { func (z *Zone) SetGateway(gatewayID int, enabled bool) error {
var err error var err error
var found bool var found bool
@ -59,8 +56,8 @@ func (z *Zone) SetGateway(gatewayID rings.NodeID, enabled bool) error {
} }
// GatewayIDs returns the list of IDs of machines that act as ring0 gateways // GatewayIDs returns the list of IDs of machines that act as ring0 gateways
func (z *Zone) GatewayIDs() ([]rings.NodeID, int) { func (z *Zone) GatewayIDs() ([]int, int) {
var out []rings.NodeID var out []int
z.ForEachMachine(func(p *Machine) bool { z.ForEachMachine(func(p *Machine) bool {
if p.IsGateway() { if p.IsGateway() {
out = append(out, p.ID) out = append(out, p.ID)
@ -70,38 +67,3 @@ func (z *Zone) GatewayIDs() ([]rings.NodeID, int) {
return out, len(out) return out, len(out)
} }
// RegionID returns the primary [Region] of a [Zone].
func (z *Zone) RegionID() rings.RegionID {
if z != nil && z.region != nil {
return z.region.ID
}
return 0
}
// Is checks if the given [rings.RegionID] and [rings.ZoneID] match
// the [Zone].
func (z *Zone) Is(regionID rings.RegionID, zoneID rings.ZoneID) bool {
switch {
case z.ID != zoneID:
return false
case z.RegionID() != regionID:
return false
default:
return true
}
}
// Eq checks if two [Zone]s are the same.
func (z *Zone) Eq(z2 *Zone) bool {
switch {
case z == nil, z2 == nil:
return false
case z.ID != z2.ID:
return false
case z.RegionID() != z2.RegionID():
return false
default:
return true
}
}

77
pkg/rings/cidr.go

@ -1,77 +0,0 @@
package rings
import "net/netip"
// AddrFromU32 converts a 32bit value into an IPv4
// address.
func AddrFromU32(v uint32) netip.Addr {
return AddrFrom4(
uint(v>>24),
uint(v>>16),
uint(v>>8),
uint(v),
)
}
// AddrFrom4 assembles an IPv4 address for 4 numbers.
// each number is truncated to 8-bits.
func AddrFrom4(a, b, c, d uint) netip.Addr {
return netip.AddrFrom4([4]byte{
byte(a & 0xff),
byte(b & 0xff),
byte(c & 0xff),
byte(d & 0xff),
})
}
// AddrToU32 converts a valid IPv4 address into it's
// 32bit numeric representation.
func AddrToU32(addr netip.Addr) (v uint32, ok bool) {
if addr.IsValid() {
if addr.Is4() || addr.Is4In6() {
a4 := addr.As4()
v = uint32(a4[0])<<24 +
uint32(a4[1])<<16 +
uint32(a4[2])<<8 +
uint32(a4[3])
return v, true
}
}
return 0, false
}
// PrefixToRange returns the beginning and end of a
// [netip.Prefix] (aka CIDR or subnet).
func PrefixToRange(subnet netip.Prefix) (from, to netip.Addr, ok bool) {
var u uint32
addr := subnet.Addr()
if u, ok = AddrToU32(addr); ok {
bits := subnet.Bits()
switch {
case bits > 32, bits < 0:
// bad
case bits == 32:
// single
from, to, ok = addr, addr, true
default:
// subnet
shift := 32 - bits
m1 := uint32((1 << shift) - 1)
m0 := uint32(0xffffffff) & ^m1
u0 := u & m0
u1 := u0 + m1
ok = true
from = AddrFromU32(u0)
to = AddrFromU32(u1)
}
}
return from, to, ok
}

178
pkg/rings/cidr_test.go

@ -1,178 +0,0 @@
package rings
import (
"fmt"
"net/netip"
"testing"
)
func TestAddrFrom4(t *testing.T) {
cases := []struct {
v [4]uint
s string
}{
{[4]uint{0, 0, 0, 0}, "0.0.0.0"},
{[4]uint{127, 0, 0, 1}, "127.0.0.1"},
{[4]uint{4096 + 127, 0, 0, 1}, "127.0.0.1"},
{[4]uint{257, 258, 259, 260}, "1.2.3.4"},
{[4]uint{255, 255, 255, 255}, "255.255.255.255"},
}
for i, tc := range cases {
fn := fmt.Sprintf("%v.%v.%v.%v", tc.v[0], tc.v[1], tc.v[2], tc.v[3])
addr := AddrFrom4(tc.v[0], tc.v[1], tc.v[2], tc.v[3])
s := addr.String()
if s == tc.s {
t.Logf("[%v/%v]: %s → %s", i, len(cases), fn, s)
} else {
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), fn, s, tc.s)
}
}
}
func TestAddrU32Invalid(t *testing.T) {
cases := []netip.Addr{
{},
netip.IPv6Unspecified(),
netip.IPv6Loopback(),
}
for i, tc := range cases {
v, ok := AddrToU32(tc)
switch {
case !ok && v == 0:
t.Logf("[%v/%v]: %s → %v %v", i, len(cases), tc, 0, false)
default:
t.Errorf("ERROR: [%v/%v]: %s → %v %v (expected %v %v)", i, len(cases),
tc, v, ok, 0, false)
}
}
}
func TestAddrU32Valid(t *testing.T) {
cases := []netip.Addr{
netip.IPv4Unspecified(),
AddrFrom4(0, 0, 0, 0),
AddrFrom4(1, 2, 3, 4),
AddrFrom4(10, 20, 30, 40),
AddrFrom4(127, 0, 0, 1),
AddrFrom4(255, 255, 255, 255),
MustParseAddr("::ffff:1.2.3.4"),
}
for i, tc := range cases {
u32, ok := AddrToU32(tc)
if !ok {
t.Errorf("ERROR: [%v/%v]: %s → %v %v", i, len(cases), tc, u32, ok)
continue
}
addr := AddrFromU32(u32)
if tc.Is4In6() {
ok = addr.Compare(tc.Unmap()) == 0
} else {
ok = addr.Compare(tc) == 0
}
if ok {
t.Logf("[%v/%v]: %s → %v → %s", i, len(cases), tc, u32, addr)
} else {
t.Errorf("ERROR: [%v/%v]: %s → %v → %s", i, len(cases), tc, u32, addr)
}
}
}
func MustParseAddr(s string) netip.Addr {
addr, err := netip.ParseAddr(s)
if err != nil {
panic(err)
}
return addr
}
func MustParsePrefix(s string) netip.Prefix {
subnet, err := netip.ParsePrefix(s)
if err != nil {
panic(err)
}
return subnet
}
func TestPrefixToRangeValid(t *testing.T) {
cases := []struct {
subnet netip.Prefix
from netip.Addr
to netip.Addr
}{
{
MustParsePrefix("127.0.0.1/32"),
MustParseAddr("127.0.0.1"),
MustParseAddr("127.0.0.1"),
},
{
MustParsePrefix("127.0.0.1/24"),
MustParseAddr("127.0.0.0"),
MustParseAddr("127.0.0.255"),
},
{
MustParsePrefix("127.0.1.2/16"),
MustParseAddr("127.0.0.0"),
MustParseAddr("127.0.255.255"),
},
{
MustParsePrefix("127.1.2.3/8"),
MustParseAddr("127.0.0.0"),
MustParseAddr("127.255.255.255"),
},
{
MustParsePrefix("10.20.30.40/12"),
MustParseAddr("10.16.0.0"),
MustParseAddr("10.31.255.255"),
},
{
MustParsePrefix("10.20.30.40/20"),
MustParseAddr("10.20.16.0"),
MustParseAddr("10.20.31.255"),
},
{
MustParsePrefix("10.0.0.0/12"),
MustParseAddr("10.0.0.0"),
MustParseAddr("10.15.255.255"),
},
{
MustParsePrefix("10.16.0.0/12"),
MustParseAddr("10.16.0.0"),
MustParseAddr("10.31.255.255"),
},
{
MustParsePrefix("10.32.0.0/12"),
MustParseAddr("10.32.0.0"),
MustParseAddr("10.47.255.255"),
},
{
MustParsePrefix("10.48.0.0/12"),
MustParseAddr("10.48.0.0"),
MustParseAddr("10.63.255.255"),
},
}
for i, tc := range cases {
from, to, ok := PrefixToRange(tc.subnet)
if ok && from.IsValid() && to.IsValid() &&
from.Compare(tc.from) == 0 &&
to.Compare(tc.to) == 0 {
//
t.Logf("[%v/%v]: %s → %s - %s",
i, len(cases),
tc.subnet,
from, to)
} else {
t.Errorf("ERROR: [%v/%v]: %q → %s - %s %v (expected %s - %s %v)",
i, len(cases),
tc.subnet,
from, to, ok,
tc.from, tc.to, true)
}
}
}

122
pkg/rings/decode.go

@ -1,122 +0,0 @@
package rings
import (
"net/netip"
)
// DecodeAddress extracts ring address fields from a given 10.0.0.0/8
// address.
//
// revive:disable:function-result-limit
func DecodeAddress[T ~uint | NodeID](addr netip.Addr) (RingID, RegionID, ZoneID, T) {
// revive:enable:function-result-limit
if addr.IsValid() {
if addr.Is4In6() {
addr = addr.Unmap()
}
if addr.Is4() {
a4 := addr.As4()
return unsafeDecodeAddress[T](a4[0], a4[1], a4[2], a4[3])
}
}
return UnspecifiedRingID, 0, 0, 0
}
// revive:disable:function-result-limit
func unsafeDecodeAddress[T ~uint | NodeID](a, b, c, d byte) (RingID, RegionID, ZoneID, T) {
// revive:enable:function-result-limit
switch {
case a != 10:
return UnspecifiedRingID, 0, 0, 0
case b == 0x00:
// 10.00.RZ.dd
k := RingZeroID
r := RegionID(c >> 4)
z := ZoneID(c & 0xf)
n := T(d)
return k, r, z, n
case b&0xf0 != 0:
// 10.Rb.cc.dd
k := RingThreeID
r := RegionID(b >> 4)
n2 := T(b & 0x0f)
n1 := T(c)
n0 := T(d)
n := n0 + n1<<8 + n2<<16
return k, r, 0, n
case c&0xf0 != 0:
// 10.0R.Zc.dd
k := RingOneID
r := RegionID(b)
z := ZoneID(c >> 4)
n1 := T(c & 0x0f)
n0 := T(d)
n := n0 + n1<<8
return k, r, z, n
default:
// 10.0R.0c.dd
k := RingTwoID
r := RegionID(b)
n1 := T(c & 0x0f)
n0 := T(d)
n := n0 + n1<<8
return k, r, 0, n
}
}
// DecodeRingZeroAddress attempts to extract region, zone and node identifiers
// from a given ring 0 address.
//
// revive:disable:function-result-limit
func DecodeRingZeroAddress(addr netip.Addr) (RegionID, ZoneID, NodeID, bool) {
// revive:enable:function-result-limit
k, r, z, n := DecodeAddress[NodeID](addr)
if k == RingZeroID {
return r, z, n, true
}
return 0, 0, 0, false
}
// DecodeRingOneAddress attempts to extract region, zone and node identifiers
// from a given ring 1 address.
//
// revive:disable:function-result-limit
func DecodeRingOneAddress(addr netip.Addr) (RegionID, ZoneID, NodeID, bool) {
// revive:enable:function-result-limit
k, r, z, n := DecodeAddress[NodeID](addr)
if k == RingOneID {
return r, z, n, true
}
return 0, 0, 0, false
}
// DecodeRingTwoAddress attempts to extract region and unique identifier for
// a kubernetes service from a given ring 2 address.
func DecodeRingTwoAddress(addr netip.Addr) (RegionID, uint, bool) {
k, r, _, n := DecodeAddress[uint](addr)
if k == RingTwoID {
return r, n, true
}
return 0, 0, false
}
// DecodeRingThreeAddress attempts to extract region and unique identifier for
// a kubernetes pod from a given ring 3 address.
func DecodeRingThreeAddress(addr netip.Addr) (RegionID, uint, bool) {
k, r, _, n := DecodeAddress[uint](addr)
if k == RingThreeID {
return r, n, true
}
return 0, 0, false
}

53
pkg/rings/decode_test.go

@ -1,53 +0,0 @@
package rings
import (
"fmt"
"net/netip"
"testing"
)
func TestDecodeRingZeroAddress(t *testing.T) {
RZNDecodeTest(t, "DecodeRingZeroAddress", DecodeRingZeroAddress, []RZNDecodeTestCase{
{1, 1, 50, MustParseAddr("10.0.17.50"), true},
{1, 2, 50, MustParseAddr("10.0.18.50"), true},
{2, 3, 1, MustParseAddr("10.0.35.1"), true},
})
}
func TesDecodetRingOneAddress(t *testing.T) {
RZNDecodeTest(t, "DecodeRingOneAddress", DecodeRingOneAddress, []RZNDecodeTestCase{
{1, 1, 50, MustParseAddr("10.1.16.50"), true},
{1, 2, 50, MustParseAddr("10.1.32.50"), true},
{2, 3, 300, MustParseAddr("10.2.49.44"), true},
})
}
type RZNDecodeTestCase struct {
region RegionID
zone ZoneID
node NodeID
addr netip.Addr
ok bool
}
func RZNDecodeTest(t *testing.T,
fnName string, fn func(netip.Addr) (RegionID, ZoneID, NodeID, bool),
cases []RZNDecodeTestCase) {
//
for i, tc := range cases {
s := fmt.Sprintf("%s(%q)", fnName, tc.addr)
r, z, n, ok := fn(tc.addr)
switch {
case ok != tc.ok, r != tc.region, z != tc.zone, n != tc.node:
t.Errorf("ERROR: [%v/%v]: %s → %v %v %v %v (expected %v %v %v %v)",
i, len(cases), s,
r, z, n, ok,
tc.region, tc.zone, tc.node, tc.ok)
default:
t.Logf("[%v/%v]: %s → %v %v %v %v", i, len(cases), s,
r, z, n, ok)
}
}
}

154
pkg/rings/encode.go

@ -1,154 +0,0 @@
package rings
import "net/netip"
// RingZeroPrefix represents the backbone that connects gateways
// of the different Ring 1 networks.
//
// The ring 0 network corresponds to what would be ring 2 for region_id 0.
// 10.0.0.0-10.0.255.255
func RingZeroPrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := UnsafeRingZeroAddress(region, zone, 0)
cidr = netip.PrefixFrom(addr, RingZeroBits)
}
return cidr, err
}
// RingZeroAddress returns a Ring 0 address for a particular node.
//
// A ring 0 address looks like 10.0.(region_id << 4 + zone_id).(node_id)/20
func RingZeroAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.ValidZero():
err = ErrOutOfRange(node, "node")
default:
addr = UnsafeRingZeroAddress(region, zone, node)
}
return addr, err
}
// RingOnePrefix represents a (virtual) local network of a zone.
//
// Ring 1 is `10.(region_id).(zone_id << 4).(node_id)/20` network
// grouped under what would be Ring 2 for region_id 0.
// There are 12 bits worth of nodes but nodes under 255 are special
// as they also get a slot on Ring 0.
func RingOnePrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := UnsafeRingOneAddress(region, zone, 0)
cidr = netip.PrefixFrom(addr, RingOneBits)
}
return cidr, err
}
// RingOneAddress returns a Ring 1 address for a particular node.
//
// A ring 1 address is `10.(region_id).(zone_id << 4).(node_id)/20`
// but the node_id can take up to 12 bits.
func RingOneAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.Valid():
err = ErrOutOfRange(node, "node")
default:
addr = UnsafeRingOneAddress(region, zone, node)
}
return addr, err
}
// RingTwoPrefix represents the services of a cluster
//
// Ring 2 subnets are of the form `10.(region_id).0.0/20`,
// using the address space that would belong to the ring 3
// region_id 0.
func RingTwoPrefix(region RegionID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := UnsafeRingTwoAddress(region, 0)
cidr = netip.PrefixFrom(addr, RingTwoBits)
}
return cidr, err
}
// RingThreePrefix returns the subnet corresponding to
// the pods of a cluster.
//
// Ring 3 is a `10.(region_id << 4).0.0/12` network
func RingThreePrefix(region RegionID) (subnet netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := UnsafeRingThreeAddress(region, 0)
subnet = netip.PrefixFrom(addr, RingThreeBits)
}
return subnet, err
}
// UnsafeRingZeroAddress is equivalent ot RingZeroAddress but without validating
// the input.
func UnsafeRingZeroAddress(region RegionID, zone ZoneID, node NodeID) netip.Addr {
r := uint(region)
z := uint(zone)
n := uint(node)
return AddrFrom4(10, 0, r<<4+z, n)
}
// UnsafeRingOneAddress is equivalent ot RingOneAddress but without validating
// the input.
func UnsafeRingOneAddress(region RegionID, zone ZoneID, node NodeID) netip.Addr {
r := uint(region)
z := uint(zone)
n := uint(node)
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r, z<<4+n1, n0)
}
// UnsafeRingTwoAddress is equivalent ot RingTwoAddress but without validating
// the input.
func UnsafeRingTwoAddress(region RegionID, n uint) netip.Addr {
r := uint(region)
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r, n1, n0)
}
// UnsafeRingThreeAddress is equivalent ot RingThreeAddress but without validating
// the input.
func UnsafeRingThreeAddress(region RegionID, n uint) netip.Addr {
r := uint(region)
n2 := n >> 16
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r<<4+n2, n1, n0)
}

63
pkg/rings/encode_test.go

@ -1,63 +0,0 @@
package rings
import (
"fmt"
"net/netip"
"testing"
)
func TestRingZeroAddress(t *testing.T) {
RZNTest(t, "RingZeroAddress", RingZeroAddress, []RZNTestCase{
{1, 1, 50, MustParseAddr("10.0.17.50")},
{1, 2, 50, MustParseAddr("10.0.18.50")},
{2, 3, 1, MustParseAddr("10.0.35.1")},
{2, 3, 300, netip.Addr{}},
})
}
func TestRingOneAddress(t *testing.T) {
RZNTest(t, "RingOneAddress", RingOneAddress, []RZNTestCase{
{1, 1, 50, MustParseAddr("10.1.16.50")},
{1, 2, 50, MustParseAddr("10.1.32.50")},
{2, 3, 300, MustParseAddr("10.2.49.44")},
{1, 20, 50, netip.Addr{}},
})
}
type RZNTestCase struct {
region RegionID
zone ZoneID
node NodeID
addr netip.Addr
}
func RZNTest(t *testing.T,
fnName string, fn func(RegionID, ZoneID, NodeID) (netip.Addr, error),
cases []RZNTestCase) {
//
for i, tc := range cases {
s := fmt.Sprintf("%s(%v, %v, %v)", fnName,
tc.region,
tc.zone,
tc.node,
)
addr, err := fn(tc.region, tc.zone, tc.node)
switch {
case !tc.addr.IsValid():
// expect error
if err != nil {
t.Logf("[%v/%v]: %s → %s", i, len(cases), s, err)
} else {
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, addr, "error")
}
case err != nil:
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, err, tc.addr)
case addr.Compare(tc.addr) != 0:
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, addr, tc.addr)
default:
t.Logf("[%v/%v]: %s → %s", i, len(cases), s, addr)
}
}
}

116
pkg/rings/rings.go

@ -1,116 +0,0 @@
// Package rings provides logic to work with the four rings
// of a cluster
package rings
import (
"fmt"
"strconv"
"syscall"
"darvaza.org/core"
)
const (
// UnspecifiedRingID is the zero value of RingID and not considered
// valid.
UnspecifiedRingID RingID = iota
RingZeroID // RingZeroID is the RingID for RingZero (backbone)
RingOneID // RingOneID is the RingID for RingOne (local zone)
RingTwoID // RingTwoID is the RingID for RingTwo (region services)
RingThreeID // RingThreeID is the RingID for RingThree (region cluster pods)
// RingMax indicates the highest [Ring] identifier
RingMax = RingThreeID
// RegionMax indicates the highest number that can be used for a [RegionID].
RegionMax = (1 << 4) - 1
// ZoneMax indicates the highest number that can be used for a [ZoneID].
ZoneMax = (1 << 4) - 1
// NodeMax indicates the highest number that can be used for a [NodeID].
NodeMax = (1 << 12) - 2
// NodeZeroMax indicates the highest number that can be used for a [NodeID]
// when its a gateway connected to Ring 0 (backbone).
NodeZeroMax = (1 << 8) - 2
// RingZeroBits indicates the size of the prefix on the ring 0 (backbone) network.
RingZeroBits = 16
// RingOneBits indicates the size of the prefix on the ring 1 (lan) network.
RingOneBits = 20
// RingTwoBits indicates the size of the prefix on the ring 2 (services) network
// of all kubernetes clusters.
RingTwoBits = 20
// RingThreeBits indicates the size of the prefix on the ring 3 (pods) network
// of the kubernetes cluster of a region.
RingThreeBits = 12
)
// RingID identifies a Ring
type RingID int
// Valid tells a [RingID] is within the valid range.
func (n RingID) Valid() bool { return n > 0 && n <= RingMax }
func (n RingID) String() string {
return idString(n)
}
// A Ring identifies what ring an address belongs to
type Ring interface {
ID() RingID
}
// RegionID is the identifier of a region, valid between 1 and [RegionMax].
type RegionID int
// Valid tells a [RegionID] is within the valid range.
func (n RegionID) Valid() bool { return n > 0 && n <= RegionMax }
func (n RegionID) String() string {
return idString(n)
}
// ZoneID is the identifier of a zone within a region, valid between 1 and [ZoneMax].
type ZoneID int
// Valid tells a [ZoneID] is within the valid range.
func (n ZoneID) Valid() bool { return n > 0 && n <= ZoneMax }
func (n ZoneID) String() string {
return idString(n)
}
// NodeID is the identifier of a machine within a zone of a region, valid between
// 1 and [NodeMax], but between 1 and [NodeZeroMax] if it will be a zone gateway.
type NodeID int
// Valid tells a [NodeID] is within the valid range.
func (n NodeID) Valid() bool { return n > 0 && n <= NodeMax }
// ValidZero tells a [NodeID] is within the valid range for a gateway.
func (n NodeID) ValidZero() bool { return n > 0 && n <= NodeZeroMax }
func (n NodeID) String() string {
return idString(n)
}
// ErrOutOfRange is an error indicating the value of a field
// is out of range.
func ErrOutOfRange[T ~int | ~uint32](value T, field string) error {
return core.Wrap(syscall.EINVAL, "%s out of range (%v)", field, value)
}
type intID interface {
~int
Valid() bool
}
func idString[T intID](p T) string {
switch {
case p == 0:
return "unspecified"
case p.Valid():
return strconv.Itoa(int(p))
default:
return fmt.Sprintf("invalid (%v)", int(p))
}
}

72
pkg/tools/buffer.go

@ -1,72 +0,0 @@
package tools
import (
"bytes"
"fmt"
"io"
)
// LazyBuffer is a [bytes.Buffer] that minimizes counting and error checks.
type LazyBuffer bytes.Buffer
// Sys returns the underlying [bytes.Buffer].
func (buf *LazyBuffer) Sys() *bytes.Buffer {
if buf == nil {
return nil
}
return (*bytes.Buffer)(buf)
}
// Len tells the size in bytes of the currently stored data.
func (buf *LazyBuffer) Len() int { return buf.Sys().Len() }
// String returns the stored data as string.
func (buf *LazyBuffer) String() string { return buf.Sys().String() }
// Bytes returns the stored data as a bytes slice.
func (buf *LazyBuffer) Bytes() []byte { return buf.Sys().Bytes() }
// Write implements the standard io.Writer interface.
func (buf *LazyBuffer) Write(b []byte) (int, error) { return buf.Sys().Write(b) }
// WriteTo implements the standard WriteTo() interface.
func (buf *LazyBuffer) WriteTo(out io.Writer) (int64, error) { return buf.Sys().WriteTo(out) }
// Print appends the [fmt.Print] equivalent to the buffer.
func (buf *LazyBuffer) Print(a ...any) error {
_, err := fmt.Fprint(buf.Sys(), a...)
return err
}
// Println appends the [fmt.Println] equivalent to the buffer.
func (buf *LazyBuffer) Println(a ...any) error {
_, err := fmt.Fprintln(buf.Sys(), a...)
return err
}
// Printf appends the [fmt.Printf] equivalent to the buffer.
func (buf *LazyBuffer) Printf(format string, a ...any) error {
_, err := fmt.Fprintf(buf.Sys(), format, a...)
return err
}
// WriteRunes appends the given runes as UTF-8 characters to the buffer.
func (buf *LazyBuffer) WriteRunes(runes ...rune) {
for _, r := range runes {
_, _ = buf.Sys().WriteRune(r)
}
}
// WriteBytes writes the given byte arrays to the buffer.
func (buf *LazyBuffer) WriteBytes(s ...[]byte) {
for _, b := range s {
_, _ = buf.Sys().Write(b)
}
}
// WriteStrings writes the given strings as UTF-8 to the buffer.
func (buf *LazyBuffer) WriteStrings(strings ...string) {
for _, s := range strings {
_, _ = buf.Sys().WriteString(s)
}
}

3
pkg/tools/gen_index.sh

@ -5,7 +5,8 @@ set -eu
: ${GO:=go} : ${GO:=go}
MODULES=$(find * -name go.mod -exec dirname '{}' \;) MODULES=$(find * -name go.mod -exec dirname '{}' \;)
GROUPS="" GROUPS="pkg cmd"
BASE="$PWD"
mod() { mod() {
local d="${1:-.}" local d="${1:-.}"

140
pkg/tools/gen_mk.sh

@ -7,12 +7,6 @@ INDEX="$1"
PROJECTS="$(cut -d':' -f1 "$INDEX")" PROJECTS="$(cut -d':' -f1 "$INDEX")"
COMMANDS="tidy get build test up" COMMANDS="tidy get build test up"
TAB=$(printf "\t")
escape_dir() {
echo "$1" | sed -e 's|/|\\/|g' -e 's|\.|\\.|g'
}
expand() { expand() {
local prefix="$1" suffix="$2" local prefix="$1" suffix="$2"
local x= out= local x= out=
@ -47,6 +41,12 @@ packed_oneline() {
packed | tr '\n' ';' | sed -e 's|;$||' -e 's|then;|then |g' -e 's|;[ \t]*|; |g' packed | tr '\n' ';' | sed -e 's|;$||' -e 's|then;|then |g' -e 's|;[ \t]*|; |g'
} }
gen_install_tools() {
cat <<EOT
for url in \$(GO_INSTALL_URLS); do \$(GO) install -v \$\$url; done
EOT
}
gen_revive_exclude() { gen_revive_exclude() {
local self="$1" local self="$1"
local dirs= d= local dirs= d=
@ -61,71 +61,36 @@ gen_revive_exclude() {
done done
} }
gen_var_name() { for cmd in $COMMANDS; do
local x= all="$(prefixed $cmd $PROJECTS)"
for x; do depsx=
echo "$x" | tr 'a-z-' 'A-Z_'
done
}
# generate files lists
#
gen_files_lists() {
local name= dir= mod= deps=
local files= files_cmd=
local filter= out_pat=
cat <<EOT cat <<EOT
GO_FILES = \$(shell find * \\ .PHONY: $cmd $all
-type d -name node_modules -prune -o \\ $cmd: $all
-type f -name '*.go' -print )
EOT EOT
while IFS=: read name dir mod deps; do
files=GO_FILES_$(gen_var_name "$name")
filter="-e '/^\.$/d;'"
[ "x$dir" = "x." ] || filter="$filter -e '/^$(escape_dir "$dir")$/d;'"
out_pat="$(cut -d: -f2 "$INDEX" | eval "sed $filter -e 's|$|/%|'" | tr '\n' ' ' | sed -e 's| \+$||')"
if [ "x$dir" = "x." ]; then
# root
files_cmd="\$(GO_FILES)"
files_cmd="\$(filter-out $out_pat, $files_cmd)"
else
files_cmd="\$(filter $dir/%, \$(GO_FILES))"
files_cmd="\$(filter-out $out_pat, $files_cmd)"
files_cmd="\$(patsubst $dir/%,%,$files_cmd)"
fi
cat <<-EOT
$files$TAB=$TAB$files_cmd
EOT
done < "$INDEX" | column -t -s "$TAB" -o " "
}
gen_make_targets() {
local cmd="$1" name="$2" dir="$3" mod="$4" deps="$5"
local call= callu= callx=
local depsx= cmdx=
local sequential=
# default calls # default calls
case "$cmd" in case "$cmd" in
tidy) tidy)
# unconditional call="$(cat <<-EOT | packed
callu="\$(GO) mod tidy" \$(GO) mod tidy
# go vet and revive only if there are .go files # go vet and revive only if there are .go files
# #
call="$(cat <<-EOT | packed $(cat <<-EOL | packed_oneline
\$(GO) vet ./... set -e
\$(GOLANGCI_LINT) run FILES="\$\$(\$(GO) list -f '{{len .GoFiles}}' ./...)"
\$(REVIVE) \$(REVIVE_RUN_ARGS) ./... if [ -n "\$\$FILES" ]; then
\$(GO) vet ./...
\$(REVIVE) \$(REVIVE_RUN_ARGS) ./...
fi
EOL
)
EOT EOT
)" )"
depsx="fmt \$(REVIVE)"
depsx="fmt"
;; ;;
up) up)
call="\$(GO) get -u -v ./... call="\$(GO) get -u -v ./...
@ -146,6 +111,10 @@ gen_make_targets() {
sequential=false ;; sequential=false ;;
esac esac
while IFS=: read name dir mod deps; do
deps=$(echo "$deps" | tr ',' ' ')
# cd $dir # cd $dir
if [ "." = "$dir" ]; then if [ "." = "$dir" ]; then
# root # root
@ -154,6 +123,7 @@ gen_make_targets() {
cd="cd '$dir'; " cd="cd '$dir'; "
fi fi
callx="$call"
if [ "$name" = root ]; then if [ "$name" = root ]; then
# special case # special case
case "$cmd" in case "$cmd" in
@ -170,20 +140,17 @@ gen_make_targets() {
[ -z "$cmdx" ] || cmdx="\$(GO) $cmdx -v ./..." [ -z "$cmdx" ] || cmdx="\$(GO) $cmdx -v ./..."
case "$cmd" in if [ "up" = "$cmd" ]; then
up)
callx="$cmdx callx="$cmdx
\$(GO) mod tidy" \$(GO) mod tidy
;; $(gen_install_tools)"
get) elif [ "get" = "$cmd" ]; then
callx="$cmdx" callx="$cmdx
;; $(gen_install_tools)"
*) elif [ -n "$cmdx" ]; then
callx="$call" classx="$cmdx"
;; fi
esac
else
callx="$call"
fi fi
if [ "build" = "$cmd" ]; then if [ "build" = "$cmd" ]; then
@ -214,45 +181,16 @@ gen_make_targets() {
deps= deps=
fi fi
files=GO_FILES_$(gen_var_name "$name")
cat <<EOT cat <<EOT
$cmd-$name:${deps:+ $(prefixed $cmd $deps)}${depsx:+ | $depsx} ; \$(info \$(M) $cmd: $name) $cmd-$name:${deps:+ $(prefixed $cmd $deps)}${depsx:+ | $depsx} ; \$(info \$(M) $cmd: $name)
EOT $(echo "$callx" | sed -e "/^$/d;" -e "s|^|\t\$(Q) $cd|")
if [ -n "$callu" ]; then
# unconditionally
echo "$callu" | sed -e "/^$/d;" -e "s|^|\t\$(Q) $cd|"
fi
if [ -n "$callx" ]; then
# only if there are files
echo "ifneq (\$($files),)"
echo "$callx" | sed -e "/^$/d;" -e "s|^|\t\$(Q) $cd|"
echo "endif"
fi
}
gen_files_lists
for cmd in $COMMANDS; do
all="$(prefixed $cmd $PROJECTS)"
depsx=
cat <<EOT
.PHONY: $cmd $all
$cmd: $all
EOT EOT
while IFS=: read name dir mod deps; do
deps=$(echo "$deps" | tr ',' ' ')
gen_make_targets "$cmd" "$name" "$dir" "$mod" "$deps"
done < "$INDEX" done < "$INDEX"
done done
for x in $PROJECTS; do for x in $PROJECTS; do
cat <<EOT cat <<EOT
$x: $(suffixed $x get build tidy) $x: $(suffixed $x get build tidy)
EOT EOT
done done

2
pkg/tools/revive.toml

@ -17,7 +17,7 @@ enableAllRules = true
[rule.cyclomatic] [rule.cyclomatic]
arguments = [10] arguments = [10]
[rule.line-length-limit] [rule.line-length-limit]
arguments = [120] arguments = [100]
severity = "warning" severity = "warning"
[rule.comment-spacings] [rule.comment-spacings]
severity = "warning" severity = "warning"

14
pkg/tools/tools.go

@ -1,11 +1,7 @@
// Package tools contains helpers //go:build tools
package tools
import "io" package tools
// LazyClose closes an [io.Closer] and discards the error import (
func LazyClose(p io.Closer) { _ "github.com/mgechev/revive"
if p != nil { )
_ = p.Close()
}
}

2
pkg/wireguard/config.go

@ -78,7 +78,7 @@ type PeerConfig struct {
Name string Name string
PublicKey PublicKey PublicKey PublicKey
Endpoint EndpointAddress Endpoint EndpointAddress
AllowedIPs []netip.Prefix AllowedIPs []netip.Prefix `ini:",comma"`
} }
// EndpointAddress is a host:port pair to reach the Peer // EndpointAddress is a host:port pair to reach the Peer

100
pkg/wireguard/config_parser.go

@ -2,12 +2,14 @@ package wireguard
import ( import (
"io/fs" "io/fs"
"strconv" "log"
"asciigoat.org/ini/basic" "asciigoat.org/ini/basic"
"darvaza.org/core" "darvaza.org/core"
) )
type sectionHandler func(*Config, *basic.Section) error
var sectionMap = map[string]func(*Config, *basic.Section) error{ var sectionMap = map[string]func(*Config, *basic.Section) error{
"Interface": loadInterfaceConfSection, "Interface": loadInterfaceConfSection,
"Peer": loadPeerConfSection, "Peer": loadPeerConfSection,
@ -48,104 +50,40 @@ func loadPeerConfSection(out *Config, src *basic.Section) error {
return nil return nil
} }
// revive:disable:cyclomatic func configFieldHandler(vi any, key, value string) error
// revive:disable:cognitive-complexity
func loadInterfaceConfField(cfg *InterfaceConfig, field basic.Field) error { func loadInterfaceConfField(cfg *InterfaceConfig, field basic.Field) error {
// revive:enable:cyclomatic log.Printf("%s[%q] = %q", "Interface", field.Key, field.Value)
// revive:enable:cognitive-complexity
// TODO: refactor when asciigoat's ini parser learns to do reflection key, value := field.Key, field.Value
switch field.Key {
case "Address":
if !core.IsZero(cfg.Address) {
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.Address.UnmarshalText([]byte(field.Value)) switch key {
switch { case "Address":
case err != nil: return configFieldHandler(&cfg.Address, key, value)
return core.Wrap(err, field.Key)
default:
return nil
}
case "PrivateKey": case "PrivateKey":
if !core.IsZero(cfg.PrivateKey) { return configFieldHandler(&cfg.PrivateKey, key, value)
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.PrivateKey.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "ListenPort": case "ListenPort":
if cfg.ListenPort > 0 { return configFieldHandler(&cfg.ListenPort, key, value)
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
u64, err := strconv.ParseUint(field.Value, 10, 16)
switch {
case err != nil:
return core.Wrap(err, field.Key)
case u64 == 0:
return core.Wrap(fs.ErrInvalid, "invalid %q value", field.Key)
default:
cfg.ListenPort = uint16(u64)
return nil
}
default: default:
return core.Wrap(fs.ErrInvalid, "unknown field %q", field.Key) return core.Wrap(fs.ErrInvalid, "unknown field %q", key)
} }
} }
// revive:disable:cyclomatic
// revive:disable:cognitive-complexity
func loadPeerConfField(cfg *PeerConfig, field basic.Field) error { func loadPeerConfField(cfg *PeerConfig, field basic.Field) error {
// revive:enable:cyclomatic log.Printf("%s[%q] = %q", "Peer", field.Key, field.Value)
// revive:enable:cognitive-complexity
switch field.Key { key, value := field.Key, field.Value
case "PublicKey":
if !core.IsZero(cfg.PublicKey) {
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.PublicKey.UnmarshalText([]byte(field.Value)) switch key {
switch { case "PublicKey":
case err != nil: return configFieldHandler(&cfg.PublicKey, key, value)
return core.Wrap(err, field.Key)
default:
return nil
}
case "Endpoint": case "Endpoint":
if cfg.Endpoint.String() != "" { return configFieldHandler(&cfg.Endpoint, key, value)
return core.Wrap(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.Endpoint.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "AllowedIPs": case "AllowedIPs":
s, err := parseAllowedIPs(field.Value) return configFieldHandler(&cfg.AllowedIPs, key, value)
switch {
case err != nil:
return core.Wrap(err, field.Key)
case len(s) > 0:
cfg.AllowedIPs = append(cfg.AllowedIPs, s...)
return nil
}
default: default:
return core.Wrap(fs.ErrInvalid, "unknown field %q", field.Key) return core.Wrap(fs.ErrInvalid, "unknown field %q", field.Key)
} }
return nil
} }
func newConfigFromDocument(doc *basic.Document) (*Config, error) { func newConfigFromDocument(doc *basic.Document) (*Config, error) {

Loading…
Cancel
Save