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:
2026-09-22 17:32:44 +02:00
parent 4811f176fd
commit 5e3dd3f59c
8 changed files with 467 additions and 14 deletions
+7
View File
@@ -135,6 +135,11 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
--overlay-range the private network's range (default 10.42.0.0/16); refused --overlay-range the private network's range (default 10.42.0.0/16); refused
if it overlaps an interface or route the machine already has if it overlaps an interface or route the machine already has
--adopted raise a machine in use as an adopted node: what it runs and its
firewall stay as they are, the foundation's filter is not loaded and
the mesh guards its own ports instead, and each module is taken on it
one at a time. Without it, a machine in use is refused
The installer carries a builder, not a control plane. What raises a mesh is therefore The installer carries a builder, not a control plane. What raises a mesh is therefore
the same thing that will maintain it, and the control plane a mesh ends up running is the same thing that will maintain it, and the control plane a mesh ends up running is
one it built itself, from a repository and a commit it can name and build again. one it built itself, from a repository and a commit it can name and build again.
@@ -312,6 +317,8 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet {
} { } {
set.IntVar(p.into, p.name, *p.into, "the machine's port for "+p.what) set.IntVar(p.into, p.name, *p.into, "the machine's port for "+p.what)
} }
set.BoolVar(&opts.Adopted, "adopted", false,
"raise this machine adopted: keep what it runs and its firewall until each module is taken")
set.StringVar(&opts.OverlayRange, "overlay-range", opts.OverlayRange, set.StringVar(&opts.OverlayRange, "overlay-range", opts.OverlayRange,
"the private network's address range; must not overlap a tunnel the machine already runs") "the private network's address range; must not overlap a tunnel the machine already runs")
if opts.Answers == nil { if opts.Answers == nil {
+192
View File
@@ -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
}
+192
View File
@@ -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)
}
}
+36 -1
View File
@@ -34,6 +34,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"time" "time"
"github.com/novox/mesh-host/internal/firewall"
) )
// Step names one stage. A failure says which one, because "the bootstrap failed" is a sentence // 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 // 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. // `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) sys, err := WorkOutSystem(ctx, d.Run, o.System)
if err != nil { if err != nil {
return result, failed(StepPreflight, err) 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 { if moved.Places > 0 {
say(fmt.Sprintf(" ports %d place(s) rewritten to this node's foundation ports", moved.Places)) 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 { for _, c := range []struct {
what, path string what, path string
made bool made bool
@@ -718,7 +745,9 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// ---- 17. filter ----------------------------------------------------------------------- // ---- 17. filter -----------------------------------------------------------------------
say("filter — required, so the question is which, not whether") 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) 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) 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, " + 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.") "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.") say("what remains is somebody else's: adding nodes, and assigning what they should run.")
+7 -1
View File
@@ -72,7 +72,13 @@ func Enrol(ctx context.Context, o Options, sys system.System, control controlPla
if mentions(nodes, o.Node) { if mentions(nodes, o.Node) {
say(" already a node " + o.Node) say(" already a node " + o.Node)
} else { } 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 return out, err
} }
out.Added = true out.Added = true
+6 -2
View File
@@ -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 // 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, func prepareModule(ctx context.Context, o Options, control controlPlane, module string,
say func(string)) error { 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)
} }
+9 -3
View File
@@ -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 // ChooseAndInstallFilter picks the packet filter — required, so the question is which, not
// whether — and installs it. // 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{ filter, err := decide(Choice{
Name: "packet-filter", Name: "packet-filter",
Question: "Which packet filter should this machine run?", Question: "Which packet filter should this machine run?",
Options: []string{"nftables"}, Options: []string{"nftables"},
}, o.Answers["packet-filter"], o.Prompt, say) }, o.Answers["packet-filter"], o.Prompt, say)
if err != nil { 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. // InstallExtras installs what was asked for beyond the floor.
+18 -7
View File
@@ -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 // 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. // 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) { 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"`) array := indexOutsideStrings(bundle, `"resources"`)
if array < 0 { 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:], "[") open := indexOutsideStrings(bundle[array:], "[")
if open < 0 { 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 open += array
depth, from := 0, -1 depth := 0
previous := open from, previous = -1, open
inString, escaped, inLine, inBlock := false, false, false, false inString, escaped, inLine, inBlock := false, false, false, false
for i := open + 1; i < len(bundle); i++ { for i := open + 1; i < len(bundle); i++ {
c := bundle[i] c := bundle[i]
@@ -181,16 +192,16 @@ func removeResource(bundle []byte, id string) ([]byte, error) {
break break
} }
if isResource(bundle[from:i+1], id) { if isResource(bundle[from:i+1], id) {
return cut(bundle, previous, from, i+1), nil return previous, from, i + 1, nil
} }
previous = i + 1 previous = i + 1
from = -1 from = -1
case c == ']' && depth == 0: 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) "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. // isResource reports whether one resource's text is the one wanted.