Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,18 @@ And what a pack may ask of the runtime is a closed list rather than whatever it
can reach: `machine.PackSurface()` names eight service families, and
`internal/cli`'s `TestNoPackReachesPastTheDeclaredDriverSurface` holds the packs'
own sources against it, naming the pack, the gesture and the line. The strongest
half is not that test: a pack receives no `machine.Driver` value at all —
`emulator.Env` keeps it unexported and hands back a finished `Binding` — so the
call it would have written does not compile. A gesture the list lacks is added
to it; the pack never works around it.
half is not that test, and it took two steps. #511 closed the way to *obtain*
a driver: `emulator.Env` keeps it unexported and hands back a finished
`Binding`, so `p.binding().Driver.EnsureNetwork(…)` stopped compiling. #514
closed the way to *name* one, because until it `var _ machine.Driver` in a pack
still compiled — measured on `154c204`, `go build ./internal/providers/scaleway/`
exited 0 — which left the surface held by a convention plus a scan. The driver
interface and its five pack-facing halves are unexported now, and what leaves
the package is `machine.Runtime`, a struct rather than a narrowed interface
because a type assertion needs no name. `internal/cli`'s
`TestThePacksCannotNameTheDriver` compiles the forbidden sentence and requires
the failure. A gesture the list lacks is added to it; the pack never works
around it.

## A request, end to end

Expand Down
6 changes: 4 additions & 2 deletions docs/fourth-pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,10 @@ spreaders — which drives the whole runtime dataplane through the shared
contract alone: it boots a machine, declares a network and joins it at boot and
after boot, publishes and withdraws a public address, hands a rule set over and
re-expands it, asks for a balancer, and keeps two subnets apart. It names no
runtime, and it could not name `machine.Driver` if it tried: since #511
`emulator.Env` hands out no driver value.
runtime, and it could not name the driver if it tried: since #511
`emulator.Env` hands out no driver value, and since #514 there is no exported
name for one — `var _ machine.Driver` in a pack fails the build, which it did
not until then.

Three things it is deliberately not:

Expand Down
4 changes: 2 additions & 2 deletions docs/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ are never chosen by the emulator.

Three things changed, and the third is the one that generalises:

- `machine.Driver.Detach`, required rather than optional, asking both ownership
- the driver's `Detach`, required rather than optional, asking both ownership
questions and removing only a device the instance itself carries. Both packs
that attach now detach; the Exoscale handler had documented the gap as
unclosable ("the driver deliberately has no hot-unplug"), which is how one
Expand Down Expand Up @@ -3037,7 +3037,7 @@ its wording differs; the verdict does not.

So `capabilities.balancing` is irrelevant to this family: the pack never asks
the runtime at all, because the only call it could make is one whose refusal is
guaranteed. `machine.Balancer` needed no provider-shaped concession to reach
guaranteed. The runtime's balancing half needed no provider-shaped concession to reach
that answer, and `internal/core` gained no Exoscale knowledge — what is missing
is an address upstream does not publish, and no field of an interface can supply
one.
Expand Down
27 changes: 13 additions & 14 deletions internal/cli/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func clean(args []string, stdout io.Writer) error {
return err
}

driver, err := resolveDriver(*vm, stdout)
rt, err := resolveDriver(*vm, stdout)
if err != nil {
return err
}
Expand All @@ -70,13 +70,12 @@ func clean(args []string, stdout io.Writer) error {
// and the delete itself — so sweeping first would report the same five
// objects the issue's reproduction reports and change nothing (#455).
if *force {
if err := clearRuntimeTraps(stdout, led, driver); err != nil {
if err := clearRuntimeTraps(stdout, led, rt); err != nil {
return err
}
}

pruner, ok := driver.(machine.Pruner)
if !ok {
if !rt.Sweeps() {
// --vm off has no runtime, so there is nothing to sweep and that is not a
// failure. It became worth distinguishing when this command started
// collecting instance records as well: an operator with no machine
Expand All @@ -85,7 +84,7 @@ func clean(args []string, stdout io.Writer) error {
// the ambiguity this project refuses everywhere else.
//
// TestCleanSucceedsWithNoRuntimeToSweep fails without this.
if _, noRuntime := driver.(machine.Noop); noRuntime {
if !rt.Runs() {
// The same sentence as the swept case, because it is just as true
// with no runtime and network.sh asserts on it. An early return that
// stayed silent would make the line depend on the mode, and a caller
Expand All @@ -95,24 +94,24 @@ func clean(args []string, stdout io.Writer) error {
// it is findable and endable with no runtime answering at all.
return sweepLeftoverDHCP(stdout, led, *vm)
}
return fmt.Errorf("the %s runtime cannot be swept", driver.Name())
return fmt.Errorf("the %s runtime cannot be swept", rt.Name())
}

// Read before, so the sweep can be judged on the host rather than on its own
// return value. #426 is why: a destruction that answers success and leaves
// the object standing is invisible to every count the remover produces, and
// that is the shape this repository has now met twice.
ctx := context.Background()
before, surveyable, surveyErr := surveyRuntime(ctx, driver)
before, surveyable, surveyErr := surveyRuntime(ctx, rt)
if surveyErr != nil {
// Never an empty list on a failed read. "I could not look" and "there is
// nothing" are different facts, and reporting the first as the second is
// how an inventory once called a live account empty.
led.record(leftoverRecord{Kind: "survey", Name: driver.Name(), Attribution: "none",
led.record(leftoverRecord{Kind: "survey", Name: rt.Name(), Attribution: "none",
Stage: stageSweep, Why: whyUnreadable, Action: actionNone})
}

pruned, err := pruner.Prune(ctx)
pruned, _, err := rt.Prune(ctx)
// Reported either way: a partial sweep still removed something, and saying
// what went is what tells the operator whether to look further.
led.prose("removed %d machine(s), %d network(s), %d rule set(s)\n",
Expand All @@ -125,10 +124,10 @@ func clean(args []string, stdout io.Writer) error {
// is still there: the case no return code reveals.
// TestTheSweepNamesWhatSurvivedItsOwnSuccessfulDelete fails without this.
if surveyable && surveyErr == nil {
after, _, afterErr := surveyRuntime(ctx, driver)
after, _, afterErr := surveyRuntime(ctx, rt)
switch {
case afterErr != nil:
led.record(leftoverRecord{Kind: "survey", Name: driver.Name(), Attribution: "none",
led.record(leftoverRecord{Kind: "survey", Name: rt.Name(), Attribution: "none",
Stage: stageSweep, Why: whyUnreadable, Action: actionNone})
default:
led.recordAll(survivors(before, after), stageSweep, whySurvived, actionNone)
Expand Down Expand Up @@ -247,15 +246,15 @@ func reportStuckLeftovers(stdout io.Writer, led *ledger, vm string, doorstep boo
// A runtime that will not resolve is not a clean host and the caller must
// not read it as one, so this fails rather than skipping: the same position
// refuseRuntimeLeftovers already took for the doorstep.
driver, err := resolveDriver(vm, stdout)
rt, err := resolveDriver(vm, stdout)
if err != nil {
led.record(leftoverRecord{Kind: "survey", Name: vm, Attribution: "none",
Stage: stageDoorstep, Why: whyUnreadable, Action: actionNone})
return fmt.Errorf("could not ask the %s runtime what a previous run left: %w", vm, err)
}

if doorstep {
if err := refuseRuntimeLeftovers(stdout, led, vm, driver); err != nil {
if err := refuseRuntimeLeftovers(stdout, led, vm, rt); err != nil {
return err
}
}
Expand All @@ -275,7 +274,7 @@ func reportStuckLeftovers(stdout io.Writer, led *ledger, vm string, doorstep boo
// and TestCleanCheckReportsARuleSetHeldByATrappedNetwork fail without it;
// TestCleanCheckStaysQuietOnARuntimeNothingHoldsBeyondItsSweep is the
// accepting half, and it is the one that keeps this usable mid-run.
trapped, err := reportRuntimeTraps(stdout, led, driver)
trapped, err := reportRuntimeTraps(stdout, led, rt)
if err != nil {
return err
}
Expand Down
20 changes: 8 additions & 12 deletions internal/cli/clean_ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,12 @@ var surveyRuntime = surveyLeftovers
// surveyLeftovers reads what the runtime holds, and distinguishes the three
// outcomes rather than two. A driver that cannot survey is not an empty host:
// it is a host nobody looked at, and it says so.
func surveyLeftovers(ctx context.Context, driver machine.Driver) (machine.Leftovers, bool, error) {
surveyor, ok := driver.(machine.Surveyor)
if !ok {
return machine.Leftovers{}, false, nil
}
left, err := surveyor.Survey(ctx)
func surveyLeftovers(ctx context.Context, rt machine.Runtime) (machine.Leftovers, bool, error) {
left, asked, err := rt.Survey(ctx)
if err != nil {
return machine.Leftovers{}, true, err
return machine.Leftovers{}, asked, err
}
return left, true, nil
return left, asked, nil
}

// recordAll writes one line per object of a survey, sorted so two runs of the
Expand Down Expand Up @@ -228,11 +224,11 @@ func (l *ledger) recordAll(left machine.Leftovers, stage, why, action string) {
// The driver is resolved by the caller and passed in, since the check now asks
// this runtime two questions rather than one and resolving it twice would let
// them disagree about which host they are talking about.
func refuseRuntimeLeftovers(out io.Writer, led *ledger, vm string, driver machine.Driver) error {
func refuseRuntimeLeftovers(out io.Writer, led *ledger, vm string, rt machine.Runtime) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

left, surveyable, err := surveyRuntime(ctx, driver)
left, surveyable, err := surveyRuntime(ctx, rt)
if !surveyable {
// --vm off, and every driver that cannot be asked. Said out loud rather
// than returned in silence: a precondition that passes quietly on the
Expand All @@ -241,10 +237,10 @@ func refuseRuntimeLeftovers(out io.Writer, led *ledger, vm string, driver machin
return nil
}
if err != nil {
led.record(leftoverRecord{Kind: "survey", Name: driver.Name(), Attribution: "none",
led.record(leftoverRecord{Kind: "survey", Name: rt.Name(), Attribution: "none",
Stage: stageDoorstep, Why: whyUnreadable, Action: actionNone})
return fmt.Errorf("could not look at what the %s runtime holds, so this host cannot be called clean: %w",
driver.Name(), err)
rt.Name(), err)
}
if len(left.Machines) == 0 && len(left.Networks) == 0 {
led.prose("no machine or network of an earlier run is left on this runtime\n")
Expand Down
30 changes: 15 additions & 15 deletions internal/cli/clean_ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ func (d *sweptDriver) Prune(context.Context) (machine.Pruned, error) {
}

// withDriver points the doorstep and the sweep at a runtime a test controls.
func withDriver(t *testing.T, d machine.Driver) {
func withDriver(t *testing.T, d machine.Runtime) {
t.Helper()
previous := resolveDriver
resolveDriver = func(string, io.Writer) (machine.Driver, error) { return d, nil }
resolveDriver = func(string, io.Writer) (machine.Runtime, error) { return d, nil }
t.Cleanup(func() { resolveDriver = previous })
// And the real host read back on top of whatever quietDHCP silenced: this
// runtime is a fake holding known objects, so reading it is the point.
Expand All @@ -79,7 +79,7 @@ func withDriver(t *testing.T, d machine.Driver) {
// DHCP services says it is not about runtime objects.
func noRuntime(t *testing.T) {
t.Helper()
withDriver(t, machine.Noop{})
withDriver(t, machine.Use(machine.Noop{}))
}

// quietDHCP silences the real /proc scan: these tests are about runtime
Expand All @@ -105,7 +105,7 @@ func TestTheDoorstepRefusesAHostHoldingAPreviousRunsNetwork(t *testing.T) {
Networks: []string{"fnt-5df8d7080c7"},
Firewalls: []string{"iso-fnt-5df8d7080c7"},
}}
withDriver(t, held)
withDriver(t, machine.Use(held))

var out bytes.Buffer
err := reportStuckLeftovers(&out, newLedger(&out, false, time.Now()), "incus", true)
Expand All @@ -124,7 +124,7 @@ func TestTheDoorstepRefusesAHostHoldingAPreviousRunsNetwork(t *testing.T) {
}

// The accepting half, on the same path.
withDriver(t, &sweptDriver{})
withDriver(t, machine.Use(&sweptDriver{}))
var clean bytes.Buffer
if err := reportStuckLeftovers(&clean, newLedger(&clean, false, time.Now()), "incus", true); err != nil {
t.Fatalf("the doorstep refused a runtime holding nothing: %v (%q)", err, clean.String())
Expand All @@ -138,7 +138,7 @@ func TestTheDoorstepRefusesAHostHoldingAPreviousRunsNetwork(t *testing.T) {
// forty minutes.
func TestTheDoorstepSaysItCouldNotLookRatherThanCallingTheHostClean(t *testing.T) {
quietDHCP(t)
withDriver(t, &sweptDriver{blind: true})
withDriver(t, machine.Use(&sweptDriver{blind: true}))

var out bytes.Buffer
led := newLedger(&out, true, time.Now())
Expand All @@ -163,10 +163,10 @@ func TestTheSweepNamesWhatSurvivedItsOwnSuccessfulDelete(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
t.Setenv("XDG_STATE_HOME", "")

withDriver(t, &sweptDriver{
withDriver(t, machine.Use(&sweptDriver{
keeps: true,
left: machine.Leftovers{Networks: []string{"fnt-5df8d7080c7"}},
})
}))

var out bytes.Buffer
if err := clean([]string{"--vm", "incus", "--format", "json"}, &out); err != nil {
Expand Down Expand Up @@ -201,7 +201,7 @@ func TestTheSweepNamesWhatSurvivedItsOwnSuccessfulDelete(t *testing.T) {
// The witness: the same sweep against a runtime that really removes must
// record no survivor. Without it this test would pass on code that labels
// every object a survivor.
withDriver(t, &sweptDriver{left: machine.Leftovers{Networks: []string{"fnt-5df8d7080c7"}}})
withDriver(t, machine.Use(&sweptDriver{left: machine.Leftovers{Networks: []string{"fnt-5df8d7080c7"}}}))
var honest bytes.Buffer
if err := clean([]string{"--vm", "incus", "--format", "json"}, &honest); err != nil {
t.Fatalf("clean on a runtime that removes: %v", err)
Expand All @@ -221,14 +221,14 @@ func TestTheLedgerAnswersWhichMechanismProducesTheWaste(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
t.Setenv("XDG_STATE_HOME", "")

withDriver(t, &sweptDriver{
withDriver(t, machine.Use(&sweptDriver{
keeps: true,
left: machine.Leftovers{
Machines: []string{"feint-scw-a"},
Networks: []string{"fnt-a", "fnt-b"},
Firewalls: []string{"iso-fnt-a"},
},
})
}))

var out bytes.Buffer
if err := clean([]string{"--vm", "incus", "--format", "json"}, &out); err != nil {
Expand Down Expand Up @@ -293,10 +293,10 @@ func TestTheLedgerIsParseableEndToEnd(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
t.Setenv("XDG_STATE_HOME", "")

withDriver(t, &sweptDriver{keeps: true, left: machine.Leftovers{
withDriver(t, machine.Use(&sweptDriver{keeps: true, left: machine.Leftovers{
Machines: []string{"feint-scw-a"},
Networks: []string{"fnt-a"},
}})
}}))

var out bytes.Buffer
if err := clean([]string{"--vm", "incus", "--format", "json"}, &out); err != nil {
Expand All @@ -319,7 +319,7 @@ func TestTheLedgerIsParseableEndToEnd(t *testing.T) {

// The text half, unchanged: network.sh decides the runtime is clean on this
// exact sentence.
withDriver(t, &sweptDriver{})
withDriver(t, machine.Use(&sweptDriver{}))
var text bytes.Buffer
if err := clean([]string{"--vm", "incus"}, &text); err != nil {
t.Fatalf("clean in text mode: %v", err)
Expand Down Expand Up @@ -355,7 +355,7 @@ func TestTheLeftoverCheckMidRunIgnoresTheRunsOwnObjects(t *testing.T) {
Networks: []string{"fnt-default", "fnt-feba907ed4e"},
Firewalls: []string{"scw-31a308684ad"},
}}
withDriver(t, live)
withDriver(t, machine.Use(live))

var out bytes.Buffer
if err := reportStuckLeftovers(&out, newLedger(&out, false, time.Now()), "incus", false); err != nil {
Expand Down
36 changes: 17 additions & 19 deletions internal/cli/clean_traps.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,20 @@ const whyTrapped = "beyond-an-ordinary-command"
// Distinguishes three outcomes rather than two, like every other reader here: a
// runtime that cannot be asked is not a clean one, and it says so instead of
// answering zero.
func reportRuntimeTraps(out io.Writer, led *ledger, driver machine.Driver) (int, error) {
repairer, ok := driver.(machine.Repairer)
if !ok {
led.prose("the %s runtime cannot be asked what holds it, so nothing was asked\n", driver.Name())
return 0, nil
}
func reportRuntimeTraps(out io.Writer, led *ledger, rt machine.Runtime) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

traps, err := repairer.Traps(ctx)
traps, asked, err := rt.Traps(ctx)
if !asked {
led.prose("the %s runtime cannot be asked what holds it, so nothing was asked\n", rt.Name())
return 0, nil
}
if err != nil {
led.record(leftoverRecord{Kind: "survey", Name: driver.Name(), Attribution: "none",
led.record(leftoverRecord{Kind: "survey", Name: rt.Name(), Attribution: "none",
Stage: stageDoorstep, Why: whyUnreadable, Action: actionNone})
return 0, fmt.Errorf("could not look at what holds the %s runtime, so this host cannot be called clean: %w",
driver.Name(), err)
rt.Name(), err)
}
if len(traps) == 0 {
led.prose("nothing on this runtime is beyond an ordinary sweep\n")
Expand All @@ -88,21 +87,20 @@ func reportRuntimeTraps(out io.Writer, led *ledger, driver machine.Driver) (int,
// The announcement is not politeness. What this touches is the runtime's own
// database, so the row is printed whole before it goes and again as it goes,
// and an operator who disagrees has everything needed to put it back.
func clearRuntimeTraps(out io.Writer, led *ledger, driver machine.Driver) error {
repairer, ok := driver.(machine.Repairer)
if !ok {
return fmt.Errorf("--force has nothing to reach on the %s runtime: it cannot be asked what holds it",
driver.Name())
}
func clearRuntimeTraps(out io.Writer, led *ledger, rt machine.Runtime) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

traps, err := repairer.Traps(ctx)
traps, asked, err := rt.Traps(ctx)
if !asked {
return fmt.Errorf("--force has nothing to reach on the %s runtime: it cannot be asked what holds it",
rt.Name())
}
if err != nil {
led.record(leftoverRecord{Kind: "survey", Name: driver.Name(), Attribution: "none",
led.record(leftoverRecord{Kind: "survey", Name: rt.Name(), Attribution: "none",
Stage: stageSweep, Why: whyUnreadable, Action: actionNone})
return fmt.Errorf("could not look at what holds the %s runtime, so nothing was forced: %w",
driver.Name(), err)
rt.Name(), err)
}
var repairable []machine.Trap
for _, trap := range traps {
Expand All @@ -125,7 +123,7 @@ func clearRuntimeTraps(out io.Writer, led *ledger, driver machine.Driver) error
led.prose(" %s %s: %s\n %s\n", trap.Kind, trap.Name, trap.Why, trap.Row)
}

cleared, repairErr := repairer.Repair(ctx)
cleared, _, repairErr := rt.Repair(ctx)
for _, trap := range cleared {
led.record(trapRecord(trap, stageSweep, actionRemoved))
led.prose("removed %s %s\n", trap.Kind, trap.Name)
Expand Down
Loading
Loading