Raise a machine in use adopted: keep its firewall, load no dropping table, guard the mesh's own ports, and take only the mesh's own modules (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// What an adopted genesis changes about the foundation (novox/hq ADR 0100).
|
||||
//
|
||||
// **The firewall found on the machine stays in force.** The foundation's own filter drops by
|
||||
// default, and every base chain at a hook runs; an accept ends only its own chain and a drop in any
|
||||
// is final — so loading it would close whatever the machine serves. On an adopted machine it is
|
||||
// not loaded. Its duty, the store never reachable from outside, passes to the mesh's guard: a table
|
||||
// of the mesh's own that only refuses, and only the foundation's own ports, which genesis has just
|
||||
// checked free — so it cannot close anything the machine serves.
|
||||
|
||||
// The guard, as the controller declares it: the same ids, paths and text, so the first push
|
||||
// finds it already there and takes it over unchanged.
|
||||
const (
|
||||
guardID = declaration.AdoptionPrefix + "guard"
|
||||
guardUnitID = declaration.AdoptionPrefix + "guard-unit"
|
||||
guardRunningID = declaration.AdoptionPrefix + "guard-running"
|
||||
guardPath = "/etc/mesh/guard.nft"
|
||||
guardUnit = "mesh-guard.service"
|
||||
guardUnitPath = "/etc/systemd/system/" + guardUnit
|
||||
)
|
||||
|
||||
// AsGuard renders the mesh's refusal-only table for the given machine ports. It passes everything
|
||||
// by default; it refuses the ports except from the machine itself — its loopback and the container
|
||||
// runtime's own networks — and from the private network, known by the interface a packet arrives
|
||||
// on and never by its source address; at prerouting, ahead of the runtime's destination
|
||||
// translation, in the inet family so both address families.
|
||||
//
|
||||
// Character for character the controller's (mesh-controller internal/catalogue AsGuard); a test
|
||||
// on each side holds its copy to the same golden text.
|
||||
func AsGuard(ports []int) string {
|
||||
sorted := append([]int{}, ports...)
|
||||
sort.Ints(sorted)
|
||||
listed := make([]string, len(sorted))
|
||||
for i, p := range sorted {
|
||||
listed[i] = strconv.Itoa(p)
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("table inet mesh_guard {}\n")
|
||||
b.WriteString("delete table inet mesh_guard\n")
|
||||
b.WriteString("table inet mesh_guard {\n")
|
||||
b.WriteString("\tchain prerouting {\n")
|
||||
b.WriteString("\t\ttype filter hook prerouting priority raw; policy accept;\n")
|
||||
fmt.Fprintf(&b, "\t\tiifname != \"lo\" iifname != \"docker0\" iifname != \"br-*\" "+
|
||||
"iifname != \"mesh0\" tcp dport { %s } drop\n", strings.Join(listed, ", "))
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("}\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// guardUnitText is the unit that loads the guard. Stopping it deletes only its own table — never a
|
||||
// flush, which would take the container runtime's rules and the found firewall with it.
|
||||
func guardUnitText() string {
|
||||
return "[Unit]\n" +
|
||||
"Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100)\n" +
|
||||
"After=network-pre.target\n" +
|
||||
"Wants=network-pre.target\n" +
|
||||
"\n" +
|
||||
"[Service]\n" +
|
||||
"Type=oneshot\n" +
|
||||
"RemainAfterExit=yes\n" +
|
||||
"ExecStart=nft -f " + guardPath + "\n" +
|
||||
"ExecReload=nft -f " + guardPath + "\n" +
|
||||
"ExecStop=nft delete table inet mesh_guard\n" +
|
||||
"\n" +
|
||||
"[Install]\n" +
|
||||
"WantedBy=multi-user.target\n"
|
||||
}
|
||||
|
||||
// guardResources are the guard as three resources of kinds the host already has.
|
||||
func guardResources(ports []int) []map[string]any {
|
||||
return []map[string]any{
|
||||
{"id": guardID, "type": "file", "path": guardPath, "content": AsGuard(ports), "mode": "0644"},
|
||||
{"id": guardUnitID, "type": "file", "path": guardUnitPath, "content": guardUnitText(), "mode": "0644"},
|
||||
{"id": guardRunningID, "type": "service", "unit": guardUnit, "state": "running",
|
||||
"boot": "enabled", "restart-on": []any{guardID, guardUnitID}},
|
||||
}
|
||||
}
|
||||
|
||||
// guardAfter is where the guard goes: once the container runtime runs, before anything publishes
|
||||
// a port.
|
||||
const guardAfter = "container-runtime-running"
|
||||
|
||||
// AdoptedRewrite says what RewriteAdopted did.
|
||||
type AdoptedRewrite struct {
|
||||
Removed []string
|
||||
Guarded []int
|
||||
}
|
||||
|
||||
// RewriteAdopted makes the produced bundle one for an adopted machine: the foundation's own filter
|
||||
// taken out, and the mesh's guard put in its place, guarding the store's and the broker's
|
||||
// management ports on this node. The nftables package stays: the guard is loaded with it, and
|
||||
// installing a package loads no table. Openings are not the bundle's — the first push declares
|
||||
// them, once there is a controller to derive them.
|
||||
func RewriteAdopted(r *Rewritten, p FoundationPorts) (AdoptedRewrite, error) {
|
||||
var out AdoptedRewrite
|
||||
p = p.orDefaults()
|
||||
bundle := r.Bundle
|
||||
var err error
|
||||
for _, id := range []string{"base-filter-loaded", "base-filter"} {
|
||||
if !r.declares(id) {
|
||||
continue
|
||||
}
|
||||
if bundle, err = removeResource(bundle, id); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Removed = append(out.Removed, id)
|
||||
}
|
||||
|
||||
out.Guarded = []int{p.Store, p.Management}
|
||||
var text bytes.Buffer
|
||||
text.WriteString(",\n // The mesh's guard (novox/hq ADR 0100): this machine is adopted, so its own firewall\n" +
|
||||
" // stays in force and the foundation's filter is not loaded. The guard only refuses: the\n" +
|
||||
" // store's and the broker's management ports, except from the machine and the private network.")
|
||||
for _, res := range guardResources(out.Guarded) {
|
||||
var one bytes.Buffer
|
||||
enc := json.NewEncoder(&one)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(res); err != nil {
|
||||
return out, err
|
||||
}
|
||||
text.WriteString("\n ")
|
||||
text.Write(bytes.TrimSpace(one.Bytes()))
|
||||
text.WriteString(",")
|
||||
}
|
||||
insert := bytes.TrimSuffix(text.Bytes(), []byte(","))
|
||||
|
||||
_, _, to, err := resourceAt(bundle, guardAfter)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("the guard goes after %q, and %w", guardAfter, err)
|
||||
}
|
||||
rest := bundle[to:]
|
||||
joined := make([]byte, 0, len(bundle)+len(insert))
|
||||
joined = append(joined, bundle[:to]...)
|
||||
joined = append(joined, insert...)
|
||||
// What followed the resource — its own comma, or the end of the list — now follows the guard.
|
||||
if trimmed := bytes.TrimLeft(rest, " \t\r\n"); len(trimmed) > 0 && trimmed[0] != ',' && trimmed[0] != ']' {
|
||||
return out, fmt.Errorf("the bundle does not separate %q from what follows it the way a list does", guardAfter)
|
||||
}
|
||||
joined = append(joined, rest...)
|
||||
|
||||
parsed, err := declaration.ParseFileTrusted(joined)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("the bundle stopped being a declaration once it was made an adopted one, which is this installer's fault: %w", err)
|
||||
}
|
||||
r.Bundle, r.Declaration, r.Resources = joined, parsed, len(parsed.Resources)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// declares is whether the produced bundle names a resource.
|
||||
func (r Rewritten) declares(id string) bool {
|
||||
for _, res := range r.Declaration.Resources {
|
||||
if res.Identity() == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// genesisTakes are the modules an adopted genesis takes as it installs them: the foundation's and
|
||||
// the mesh's own, whose names genesis checked free, so taking them replaces nothing a predecessor
|
||||
// ran. The private network is not among them — it rewrites the machine's hosts file and the
|
||||
// container runtime's configuration whole — and neither is anything the operator installs later.
|
||||
var genesisTakes = map[string]bool{
|
||||
RegistryModule: true, ControlPlaneModule: true, BuilderModule: true,
|
||||
"postgres": true, "lavinmq": true, "mesh-vault": true, "mesh-catalog": true,
|
||||
}
|
||||
|
||||
// takeIfAdopted takes one of genesis's own modules on an adopted node, once it is assigned and
|
||||
// before the push that raises it.
|
||||
func takeIfAdopted(ctx context.Context, o Options, control controlPlane, module string, say func(string)) error {
|
||||
if !o.Adopted || !genesisTakes[module] {
|
||||
return nil
|
||||
}
|
||||
if _, err := control.tell(ctx, "take", o.Node, module); err != nil {
|
||||
return err
|
||||
}
|
||||
say(" taken " + module + " on " + o.Node + " — the mesh's own, its name checked free")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0100: an adopted genesis loads no table that drops by default or holds an
|
||||
// accept; the mesh guards its own ports in a table that only refuses; and genesis takes the mesh's
|
||||
// own modules as it installs them, and nothing else.
|
||||
|
||||
// The same golden text the controller's test holds its AsGuard to.
|
||||
const goldenGuard = `table inet mesh_guard {}
|
||||
delete table inet mesh_guard
|
||||
table inet mesh_guard {
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority raw; policy accept;
|
||||
iifname != "lo" iifname != "docker0" iifname != "br-*" iifname != "mesh0" tcp dport { 5432, 15672 } drop
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestTheGuardIsExactlyThisTable(t *testing.T) {
|
||||
if got := AsGuard([]int{15672, 5432}); got != goldenGuard {
|
||||
t.Fatalf("the guard changed:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedBundleLoadsNoDroppingTableAndExactlyTheGuard(t *testing.T) {
|
||||
r := producedBundle(t)
|
||||
p := FoundationPorts{Store: 5433, Management: 15673}
|
||||
if _, err := RewritePorts(&r, p, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := RewriteAdopted(&r, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(got.Removed, ",") != "base-filter-loaded,base-filter" {
|
||||
t.Errorf("removed %v", got.Removed)
|
||||
}
|
||||
at := map[string]int{}
|
||||
var guards []*declaration.File
|
||||
for i, res := range r.Declaration.Resources {
|
||||
at[res.Identity()] = i
|
||||
if f, ok := res.(*declaration.File); ok {
|
||||
if strings.Contains(f.Content, "policy drop") || strings.Contains(f.Content, " accept\n") &&
|
||||
!strings.Contains(f.Content, "policy accept") {
|
||||
t.Errorf("%s loads a table that drops or accepts: %q", f.ID, f.Content)
|
||||
}
|
||||
if f.Path == guardPath {
|
||||
guards = append(guards, f)
|
||||
}
|
||||
}
|
||||
if s, ok := res.(*declaration.Service); ok && s.Unit == "nftables.service" {
|
||||
t.Errorf("the foundation's filter is still loaded by %s", s.ID)
|
||||
}
|
||||
}
|
||||
if len(guards) != 1 {
|
||||
t.Fatalf("%d guard table(s)", len(guards))
|
||||
}
|
||||
if !strings.Contains(guards[0].Content, "tcp dport { 5433, 15673 } drop") {
|
||||
t.Errorf("the guard does not refuse this node's ports: %s", guards[0].Content)
|
||||
}
|
||||
if strings.Count(guards[0].Content, "accept") != 1 || !strings.Contains(guards[0].Content, "policy accept") {
|
||||
t.Errorf("the guard holds an accept of its own: %s", guards[0].Content)
|
||||
}
|
||||
for _, id := range []string{guardID, guardUnitID, guardRunningID} {
|
||||
if _, ok := at[id]; !ok {
|
||||
t.Errorf("the bundle has no %s", id)
|
||||
}
|
||||
}
|
||||
if !(at["container-runtime-running"] < at[guardID] && at[guardRunningID] < at["store"]) {
|
||||
t.Errorf("the guard is not between the runtime and the store: %v", at)
|
||||
}
|
||||
if _, kept := at["base-filter-package"]; !kept {
|
||||
t.Error("nft, which loads the guard, is no longer installed")
|
||||
}
|
||||
unit := r.Declaration.Resources[at[guardRunningID]].(*declaration.Service)
|
||||
if unit.Unit != guardUnit || unit.State != "running" || strings.Join(unit.RestartOn, ",") != guardID+","+guardUnitID {
|
||||
t.Errorf("the guard's service: %+v", unit)
|
||||
}
|
||||
stop := r.Declaration.Resources[at[guardUnitID]].(*declaration.File).Content
|
||||
if !strings.Contains(stop, "ExecStop=nft delete table inet mesh_guard") || strings.Contains(stop, "flush") {
|
||||
t.Errorf("stopping the guard does not delete only its own table: %s", stop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAConvergedBundleIsNotMadeAnAdoptedOne(t *testing.T) {
|
||||
// The converged genesis keeps the foundation's filter, byte for byte (novox/hq ADR 0088).
|
||||
r := producedBundle(t)
|
||||
for _, res := range r.Declaration.Resources {
|
||||
if strings.HasPrefix(res.Identity(), declaration.AdoptionPrefix) {
|
||||
t.Errorf("a converged bundle carries %s", res.Identity())
|
||||
}
|
||||
}
|
||||
if !r.declares("base-filter-loaded") {
|
||||
t.Error("a converged bundle lost its filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedGenesisTakesTheMeshsOwnModulesBeforePushingThem(t *testing.T) {
|
||||
t.Setenv("TMPDIR", t.TempDir())
|
||||
for _, c := range []struct {
|
||||
module string
|
||||
takes bool
|
||||
}{{RegistryModule, true}, {ControlPlaneModule, true}, {BuilderModule, true}, {"gitea", false}} {
|
||||
rec := &controlRecorder{settings: map[string]string{}}
|
||||
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
||||
o := Options{Node: "anchor", Adopted: true, Wait: time.Second}
|
||||
if _, err := installModule(context.Background(), o, control, c.module, []byte(`{}`), quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assign, take, push := rec.index("assign anchor "+c.module), rec.index("take anchor "+c.module), rec.index("push anchor")
|
||||
if !c.takes {
|
||||
if take >= 0 {
|
||||
t.Errorf("%s was taken at genesis", c.module)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !(assign >= 0 && assign < take && take < push) {
|
||||
t.Errorf("%s: assign %d, take %d, push %d: %v", c.module, assign, take, push, rec.told)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAConvergedGenesisTakesNothing(t *testing.T) {
|
||||
t.Setenv("TMPDIR", t.TempDir())
|
||||
rec := &controlRecorder{settings: map[string]string{}}
|
||||
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
||||
if _, err := installModule(context.Background(), Options{Node: "anchor", Wait: time.Second}, control,
|
||||
RegistryModule, []byte(`{}`), quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.index("take") >= 0 {
|
||||
t.Errorf("a converged genesis took a module: %v", rec.told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedGenesisOpensTheRegistryFromAnywhere(t *testing.T) {
|
||||
t.Setenv("TMPDIR", t.TempDir())
|
||||
rec := &controlRecorder{settings: map[string]string{}}
|
||||
control := controlPlane{container: "temp-mesh-controller", run: rec.run, timeout: time.Second}
|
||||
o := Options{Node: "anchor", Adopted: true, Ports: FoundationPorts{Registry: 5100}, Wait: time.Second}
|
||||
if _, err := installModule(context.Background(), o, control, RegistryModule, []byte(`{}`), quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := rec.settings["distribution-settings.json"]; got != `{"expose":{"5000":"anywhere"},"ports":{"5000":5100}}` {
|
||||
t.Errorf("the registry was told %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedGenesisChoosesTheFilterAndLoadsNone(t *testing.T) {
|
||||
rec := &controlRecorder{settings: map[string]string{}}
|
||||
control := controlPlane{container: "mesh-controller", run: rec.run, timeout: time.Second}
|
||||
o := Options{Node: "anchor", Adopted: true, Answers: map[string]string{"packet-filter": "nftables"}}
|
||||
filter, err := ChooseAndInstallFilter(context.Background(), o, control, quietly)
|
||||
if err != nil || filter != "nftables" {
|
||||
t.Fatalf("%q %v", filter, err)
|
||||
}
|
||||
if len(rec.told) != 0 {
|
||||
t.Errorf("an adopted genesis installed a filter: %v", rec.told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedNodeIsRecordedAdopted(t *testing.T) {
|
||||
stop := errors.New("stop here")
|
||||
runtime := &asked{answer: func(name string, args []string) (string, error) {
|
||||
joined := strings.Join(args, " ")
|
||||
switch {
|
||||
case strings.Contains(joined, "node list"):
|
||||
return "", nil
|
||||
case strings.Contains(joined, "node add"):
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("%w: %s %v", stop, name, args)
|
||||
}}
|
||||
_, _ = Enrol(context.Background(), Options{
|
||||
Node: "anchor", Adopted: true, State: t.TempDir() + "/state.json", Timeout: time.Second,
|
||||
Host: "/usr/local/bin/mesh-host", HostInBackground: true,
|
||||
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
||||
func(string) {})
|
||||
if !runtime.ran("node add anchor --adopted") {
|
||||
t.Errorf("the node was not added adopted: %v", runtime.commands)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/firewall"
|
||||
)
|
||||
|
||||
// Step names one stage. A failure says which one, because "the bootstrap failed" is a sentence
|
||||
@@ -377,6 +379,23 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
|
||||
// Which half of the host applies things here. Asked of the machine and proved, because
|
||||
// `mesh-host` pins this at link time and an installer run by hand has no link time.
|
||||
// An adopted machine keeps the firewall it was found with, so the mesh must speak it; one no
|
||||
// host speaks is refused here, before anything changes (novox/hq ADR 0100).
|
||||
if o.Adopted {
|
||||
kind, name, err := firewall.Detect(ctx, d.Run)
|
||||
if err != nil {
|
||||
return result, failed(StepPreflight, err)
|
||||
}
|
||||
if kind == firewall.Unsupported {
|
||||
return result, failed(StepPreflight, fmt.Errorf(
|
||||
"this machine is filtered by %s, and no host speaks that firewall yet. An adopted "+
|
||||
"machine keeps its firewall in force, so the mesh could neither open what it needs "+
|
||||
"through it nor say what it would close. Nothing was changed", name))
|
||||
}
|
||||
result.Firewall = string(kind)
|
||||
say(" adopted what is on this machine is kept; its firewall (" + string(kind) + ") stays in force")
|
||||
}
|
||||
|
||||
sys, err := WorkOutSystem(ctx, d.Run, o.System)
|
||||
if err != nil {
|
||||
return result, failed(StepPreflight, err)
|
||||
@@ -444,6 +463,14 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
if moved.Places > 0 {
|
||||
say(fmt.Sprintf(" ports %d place(s) rewritten to this node's foundation ports", moved.Places))
|
||||
}
|
||||
if o.Adopted {
|
||||
adopted, err := RewriteAdopted(&rewritten, o.Ports)
|
||||
if err != nil {
|
||||
return result, failed(StepBundle, err)
|
||||
}
|
||||
say(fmt.Sprintf(" adopted bundle the foundation's filter is not loaded (%s); the mesh's guard refuses %v from outside",
|
||||
strings.Join(adopted.Removed, ", "), adopted.Guarded))
|
||||
}
|
||||
for _, c := range []struct {
|
||||
what, path string
|
||||
made bool
|
||||
@@ -718,7 +745,9 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
|
||||
// ---- 17. filter -----------------------------------------------------------------------
|
||||
say("filter — required, so the question is which, not whether")
|
||||
if err := ChooseAndInstallFilter(ctx, o, permanentControl, say); err != nil {
|
||||
filter, err := ChooseAndInstallFilter(ctx, o, permanentControl, say)
|
||||
result.Filter = filter
|
||||
if err != nil {
|
||||
return result, failed(StepFilter, err)
|
||||
}
|
||||
|
||||
@@ -736,6 +765,12 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
return result, failed(StepExport, err)
|
||||
}
|
||||
|
||||
if o.Adopted {
|
||||
say("\nthis machine is a mesh of one adopted node: it builds its own software, holds its graph " +
|
||||
"and sits on its private network, and what it ran before is kept as it was, behind the firewall " +
|
||||
"it was found with. Take each module on it once its data has moved; converge it when done.")
|
||||
return result, nil
|
||||
}
|
||||
say("\nthis machine is a mesh of one node: it builds its own software, holds its graph, " +
|
||||
"sits on its private network, and filters what modules declared.")
|
||||
say("what remains is somebody else's: adding nodes, and assigning what they should run.")
|
||||
|
||||
@@ -72,7 +72,13 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla
|
||||
if mentions(nodes, o.Node) {
|
||||
say(" already a node " + o.Node)
|
||||
} else {
|
||||
if _, err := control.tell(ctx, "node", "add", o.Node); err != nil {
|
||||
add := []string{"node", "add", o.Node}
|
||||
if o.Adopted {
|
||||
// The controller records the node's mode; an adopted one keeps what it was found with
|
||||
// until each module is taken (novox/hq ADR 0100).
|
||||
add = append(add, "--adopted")
|
||||
}
|
||||
if _, err := control.tell(ctx, add...); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Added = true
|
||||
|
||||
@@ -214,8 +214,12 @@ func pinPlaceholder(manifest []byte, reference, module string) ([]byte, int, err
|
||||
}
|
||||
|
||||
// prepareModule is what genesis tells the controller about a module on this node once it is
|
||||
// assigned and before it is pushed: the ports this node gave it (novox/hq ADR 0100).
|
||||
// assigned and before it is pushed: the ports this node gave it, and on an adopted node that the
|
||||
// module is taken (novox/hq ADR 0100).
|
||||
func prepareModule(ctx context.Context, o Options, control controlPlane, module string,
|
||||
say func(string)) error {
|
||||
return setFoundationSettings(ctx, o, control, module, say)
|
||||
if err := setFoundationSettings(ctx, o, control, module, say); err != nil {
|
||||
return err
|
||||
}
|
||||
return takeIfAdopted(ctx, o, control, module, say)
|
||||
}
|
||||
|
||||
@@ -150,16 +150,22 @@ func PlaceOnTheNetwork(ctx context.Context, o Options, control controlPlane,
|
||||
|
||||
// ChooseAndInstallFilter picks the packet filter — required, so the question is which, not
|
||||
// whether — and installs it.
|
||||
func ChooseAndInstallFilter(ctx context.Context, o Options, control controlPlane, say func(string)) error {
|
||||
func ChooseAndInstallFilter(ctx context.Context, o Options, control controlPlane, say func(string)) (string, error) {
|
||||
filter, err := decide(Choice{
|
||||
Name: "packet-filter",
|
||||
Question: "Which packet filter should this machine run?",
|
||||
Options: []string{"nftables"},
|
||||
}, o.Answers["packet-filter"], o.Prompt, say)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return InstallFromCatalogue(ctx, o, control, filter, say)
|
||||
if o.Adopted {
|
||||
// The firewall found here stays in force until the node converges; the filter is
|
||||
// chosen now and assigned by the flip (novox/hq ADR 0100).
|
||||
say(" not installed " + filter + " — this machine is adopted; converging it assigns " + filter)
|
||||
return filter, nil
|
||||
}
|
||||
return filter, InstallFromCatalogue(ctx, o, control, filter, say)
|
||||
}
|
||||
|
||||
// InstallExtras installs what was asked for beyond the floor.
|
||||
|
||||
@@ -132,18 +132,29 @@ func RetireTheTemporaryControlPlane(ctx context.Context, o Options, sys system.S
|
||||
// where the comment explaining it lives. A comment that outlives the thing it describes is worse
|
||||
// than no comment: it is the file telling somebody the machine has a control plane it does not.
|
||||
func removeResource(bundle []byte, id string) ([]byte, error) {
|
||||
previous, from, to, err := resourceAt(bundle, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cut(bundle, previous, from, to), nil
|
||||
}
|
||||
|
||||
// resourceAt finds one resource's object in a bundle's text by its id: where the one before it
|
||||
// ended, and where it starts and ends — comments and strings skipped, so an id quoted in a comment
|
||||
// or a command is never mistaken for the resource.
|
||||
func resourceAt(bundle []byte, id string) (previous, from, to int, err error) {
|
||||
array := indexOutsideStrings(bundle, `"resources"`)
|
||||
if array < 0 {
|
||||
return nil, fmt.Errorf("this bundle has no resources array, so there is nothing to take out of it")
|
||||
return 0, 0, 0, fmt.Errorf("this bundle has no resources array, so there is nothing to take out of it")
|
||||
}
|
||||
open := indexOutsideStrings(bundle[array:], "[")
|
||||
if open < 0 {
|
||||
return nil, fmt.Errorf("this bundle's resources are not a list")
|
||||
return 0, 0, 0, fmt.Errorf("this bundle's resources are not a list")
|
||||
}
|
||||
open += array
|
||||
|
||||
depth, from := 0, -1
|
||||
previous := open
|
||||
depth := 0
|
||||
from, previous = -1, open
|
||||
inString, escaped, inLine, inBlock := false, false, false, false
|
||||
for i := open + 1; i < len(bundle); i++ {
|
||||
c := bundle[i]
|
||||
@@ -181,16 +192,16 @@ func removeResource(bundle []byte, id string) ([]byte, error) {
|
||||
break
|
||||
}
|
||||
if isResource(bundle[from:i+1], id) {
|
||||
return cut(bundle, previous, from, i+1), nil
|
||||
return previous, from, i + 1, nil
|
||||
}
|
||||
previous = i + 1
|
||||
from = -1
|
||||
case c == ']' && depth == 0:
|
||||
return nil, fmt.Errorf(
|
||||
return 0, 0, 0, fmt.Errorf(
|
||||
"this bundle declares no %q, so there is nothing to take out of it", id)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("this bundle's resources list does not end")
|
||||
return 0, 0, 0, fmt.Errorf("this bundle's resources list does not end")
|
||||
}
|
||||
|
||||
// isResource reports whether one resource's text is the one wanted.
|
||||
|
||||
Reference in New Issue
Block a user