A container may name networks it also joins once created, for the per-machine setting that keeps a found network while a neighbour still resolves it there: joined after the run, part of the spec, refused when it cannot be joined. A declaration may say which modules the mesh left out because a stored setting cannot compose with its definition. Absence used to read as removal; a left-out module's records are kept and said, and its holds are not released. Genesis raises the bootstrap forge under the gitea module's container name, with its image digest and its data directory mounted at /data, so the module holds it by the found rule instead of raising a second forge beside it (issue 090). The network is the one difference left for a take to say. Before this the forge had no volume: its repositories were the container's, lost with it.
180 lines
7.9 KiB
Go
180 lines
7.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0100: a converged genesis refuses a machine in use, naming every container
|
|
// and listener it counted.
|
|
|
|
// Lines as `ss -Hltunp` prints them. The ssh, samba, loopback and proxy lines are captured from a
|
|
// real machine; the resolver and network-manager lines are written in the same shape. What a fresh
|
|
// machine actually runs is measured in testdata/fresh-machine-listeners.txt.
|
|
const inUseSockets = `tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1188536,fd=6))
|
|
tcp LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1188536,fd=7))
|
|
tcp LISTEN 0 32 127.0.0.1:53 0.0.0.0:* users:(("dnsmasq",pid=1189392,fd=7))
|
|
tcp LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("docker-proxy",pid=1854543,fd=7))
|
|
udp UNCONN 0 0 0.0.0.0:5355 0.0.0.0:* users:(("systemd-resolve",pid=301,fd=11))
|
|
udp UNCONN 0 0 192.0.2.10%eth0:68 0.0.0.0:* users:(("systemd-network",pid=280,fd=19))
|
|
`
|
|
|
|
const servingSockets = `tcp LISTEN 0 50 0.0.0.0:445 0.0.0.0:* users:(("smbd",pid=1248,fd=29))
|
|
tcp LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=1920035,fd=7))
|
|
udp UNCONN 0 0 0.0.0.0:123 0.0.0.0:* users:(("ntpd",pid=1070791,fd=17))
|
|
`
|
|
|
|
type inUseRunner struct{ ps, ss string }
|
|
|
|
func (m inUseRunner) run(_ context.Context, name string, args ...string) (string, error) {
|
|
if name == "docker" {
|
|
return m.ps, nil
|
|
}
|
|
return m.ss, nil
|
|
}
|
|
|
|
func TestAFreshMachineIsNotInUse(t *testing.T) {
|
|
containers, listeners, err := InUse(context.Background(), inUseRunner{ss: inUseSockets}.run,
|
|
func(string) bool { return false })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(containers) != 0 || len(listeners) != 0 {
|
|
t.Errorf("ssh, loopback and the daemons a fresh machine runs were counted: %v %v", containers, listeners)
|
|
}
|
|
}
|
|
|
|
func TestAMachineServingIsInUse(t *testing.T) {
|
|
m := inUseRunner{ps: "hello-web\t\nmesh-store\tabc123\n", ss: inUseSockets + servingSockets}
|
|
containers, listeners, err := InUse(context.Background(), m.run, func(string) bool { return false })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(containers) != 1 || containers[0] != "hello-web" {
|
|
t.Errorf("containers counted: %v (one a host made is not a predecessor's)", containers)
|
|
}
|
|
var by []string
|
|
for _, l := range listeners {
|
|
by = append(by, l.By)
|
|
}
|
|
if strings.Join(by, " ") != "smbd docker-proxy ntpd" {
|
|
t.Errorf("listeners counted: %v", listeners)
|
|
}
|
|
}
|
|
|
|
func TestAConvergedGenesisRefusesAMachineInUseNamingEverything(t *testing.T) {
|
|
o := Options{State: filepath.Join(t.TempDir(), "state.json")}
|
|
m := inUseRunner{ps: "hello-web\t\n", ss: inUseSockets + servingSockets}
|
|
err := RefuseAMachineInUse(context.Background(), o, m.run, quietly)
|
|
if err == nil {
|
|
t.Fatal("a machine in use was not refused")
|
|
}
|
|
for _, want := range []string{"container hello-web", "tcp 0.0.0.0:445 by smbd", "tcp 0.0.0.0:8080 by docker-proxy",
|
|
"udp 0.0.0.0:123 by ntpd", "--adopted"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not name %q: %v", want, err)
|
|
}
|
|
}
|
|
o.Adopted = true
|
|
if err := RefuseAMachineInUse(context.Background(), o, m.run, quietly); err != nil {
|
|
t.Errorf("an adopted genesis was refused a machine in use: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestARerunOfGenesisIsNotAMachineInUse(t *testing.T) {
|
|
o := Options{State: filepath.Join(t.TempDir(), "state.json")}
|
|
if err := store.Save(o.State, store.State{Resources: []store.Applied{{ID: "store", Type: "container", Target: "mesh-store"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := inUseRunner{ps: giteaBootstrap + "\t\n", ss: servingSockets}
|
|
if err := RefuseAMachineInUse(context.Background(), o, m.run, quietly); err != nil {
|
|
t.Errorf("what an earlier genesis raised was counted as a machine in use: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAFreshlyInstalledMachineAsMeasuredIsNotInUse(t *testing.T) {
|
|
// Captured with `ss -Hltunp` on a freshly installed lab machine: its resolver listens on TCP on
|
|
// every address, which the record's words alone would count.
|
|
raw, err := os.ReadFile("testdata/fresh-machine-listeners.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
if name == "ss" {
|
|
return string(raw), nil
|
|
}
|
|
return "", nil
|
|
}
|
|
containers, listeners, err := InUse(context.Background(), run, func(string) bool { return false })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(containers) != 0 || len(listeners) != 0 {
|
|
t.Errorf("a fresh machine read as in use: containers %v, listeners %v", containers, listeners)
|
|
}
|
|
}
|
|
|
|
// Defends novox/hq ADR 0103: a machine raised adopted stays adopted if genesis is run again. The
|
|
// installer reads the mode from what the machine records, and refuses a flag that disagrees.
|
|
func TestARerunWithoutTheFlagOnAnAdoptedMachineIsRefused(t *testing.T) {
|
|
o := Options{State: filepath.Join(t.TempDir(), "state.json")}
|
|
adoptedState := store.State{Resources: []store.Applied{
|
|
{ID: "store", Type: "container", Target: "mesh-store", Origin: store.OriginCarried},
|
|
{ID: "adoption.guard", Type: "file", Target: "/etc/mesh/guard.nft", Origin: store.OriginCarried},
|
|
}}
|
|
if err := store.Save(o.State, adoptedState); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := inUseRunner{ss: inUseSockets}
|
|
err := RefuseAMachineInUse(context.Background(), o, m.run, quietly)
|
|
if err == nil || !strings.Contains(err.Error(), "pass --adopted") {
|
|
t.Fatalf("a re-run without --adopted on an adopted machine was not refused: %v", err)
|
|
}
|
|
o.Adopted = true
|
|
if err := RefuseAMachineInUse(context.Background(), o, m.run, quietly); err != nil {
|
|
t.Errorf("a re-run with --adopted on an adopted machine was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestARerunWithTheFlagOnAConvergedMachineIsRefused(t *testing.T) {
|
|
o := Options{State: filepath.Join(t.TempDir(), "state.json"), Adopted: true}
|
|
converged := store.State{Resources: []store.Applied{
|
|
{ID: "store", Type: "container", Target: "mesh-store", Origin: store.OriginCarried},
|
|
{ID: "base-filter", Type: "file", Target: "/etc/nftables.conf", Origin: store.OriginCarried},
|
|
},
|
|
// Converged by the controller from adopted: the firewall it found is still recorded.
|
|
Firewall: &store.FoundFirewall{Kind: "ufw", WasActive: true, DisabledByMesh: true}}
|
|
if err := store.Save(o.State, converged); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := RefuseAMachineInUse(context.Background(), o, inUseRunner{ss: inUseSockets}.run, quietly)
|
|
if err == nil || !strings.Contains(err.Error(), "without --adopted") {
|
|
t.Fatalf("a re-run with --adopted on a converged machine was not refused: %v", err)
|
|
}
|
|
o.Adopted = false
|
|
if err := RefuseAMachineInUse(context.Background(), o, inUseRunner{ss: inUseSockets}.run, quietly); err != nil {
|
|
t.Errorf("a converged re-run of a converged machine was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOnlyTheDaemonsTheMeasurementFoundAreQuiet(t *testing.T) {
|
|
// novox/hq ADR 0101: the exempt daemons are the ones a fresh machine was measured to run —
|
|
// the resolver and the network manager. A time client or a DHCP client listening beyond
|
|
// loopback is something somebody put there, and that is a machine in use.
|
|
sockets := `udp UNCONN 0 0 0.0.0.0:123 0.0.0.0:* users:(("systemd-timesyn",pid=260,fd=9))
|
|
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:* users:(("dhcpcd",pid=270,fd=9))
|
|
`
|
|
_, listeners, err := InUse(context.Background(), inUseRunner{ss: sockets}.run, func(string) bool { return false })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(listeners) != 2 {
|
|
t.Errorf("counted %d listener(s), want the time client and the DHCP client: %+v", len(listeners), listeners)
|
|
}
|
|
}
|