Hold the trust-before-push order by a test, and refuse early what would stop it (issue 92)
The fix is an order, and nothing failed if the trust call moved after the push or went: steps 7 and 8 are now one function a test drives against a runtime that refuses an untrusted push. Preflight now refuses, before anything is raised, a runtime file that is no JSON object, a docker.service that is not loaded and active, and a dockerd started with --insecure-registry, which would refuse the reload. The comment names the order the docker module's takeover rests on.
This commit is contained in:
@@ -31,10 +31,23 @@ import (
|
||||
// could hold it, because the retire step re-applies the bundle and takes away a carried record the
|
||||
// bundle does not name. The `docker` module, applied later, declares the artifact store's reach —
|
||||
// the mesh name on the node's port — finds it already in the file it has no record of, and takes it
|
||||
// as its own (ADR 0102: a declared member found in an unrecorded file is the mesh's). So nothing
|
||||
// changes on the machine at the handover, no reload; from then on the module owns that member. The
|
||||
// address the operator gave, when it is not the mesh name, stays as part of the machine's own
|
||||
// configuration, as the hand-written trust it replaces would have.
|
||||
// as its own (ADR 0102: a declared member found in an unrecorded file is the mesh's). That member
|
||||
// does not change at the handover. The module's first apply usually still changes the file and
|
||||
// reloads the runtime once, for the keys it writes beside it (`live-restore`, `log-driver`,
|
||||
// `log-opts`); a reload stops nothing. The address the operator gave, when it is not the mesh name,
|
||||
// stays as part of the machine's own configuration, as the hand-written trust it replaces would have.
|
||||
//
|
||||
// **This rests on an assumption about order.** The module takes the mesh name's member over only
|
||||
// because its FIRST apply on this node already declares it: genesis assigns no `docker` before the
|
||||
// network step (16), and once the node is on the network the controller composes the reach as
|
||||
// `<node>.<suffix>:<port>`.
|
||||
// Were `docker` applied earlier — while the reach is empty or `127.0.0.1:<port>`, as it is before the
|
||||
// network — its record would exist without the mesh name, and the member would stay the machine's
|
||||
// for good: undeclaring the module would never take it out. Installing the node's seat holders
|
||||
// earlier in genesis must revisit this.
|
||||
//
|
||||
// **The suffix** is read from this process's environment, and the controller reads its own: a mesh
|
||||
// with a custom `MESH_INTERNAL_SUFFIX` sets it for both.
|
||||
|
||||
// runtimeConfig is the container runtime's configuration, which the trust is written into. A
|
||||
// variable so a test writes into a file of its own.
|
||||
@@ -230,3 +243,75 @@ func inAny(ip net.IP, cidrs []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// procRoot is where a process's command line is read from. A variable so a test gives its own.
|
||||
var procRoot = "/proc"
|
||||
|
||||
// RuntimeWillTakeTheTrust is preflight's half of the registry's trust: everything that can be known
|
||||
// about it without writing anything, asked at step 1 so a machine that cannot take the trust is
|
||||
// refused before the foundation is raised, rather than at step 7 after it (novox/hq issue 92).
|
||||
//
|
||||
// - The runtime's file is absent or a JSON object: the mesh writes into an object, and refuses to
|
||||
// write over anything else (ADR 0102).
|
||||
// - The runtime's service is known to systemd and active: the trust is taken by reloading it.
|
||||
// - The runtime was not started with `--insecure-registry`: dockerd refuses a directive given both
|
||||
// as a flag and in its file, so it would refuse the reload and keep distrusting the registry.
|
||||
func RuntimeWillTakeTheTrust(ctx context.Context, o Options, run Runner, say func(string)) error {
|
||||
names := RegistryNames(o)
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
if raw, err := os.ReadFile(runtimeConfig); err == nil {
|
||||
var object map[string]json.RawMessage
|
||||
if len(strings.TrimSpace(string(raw))) > 0 {
|
||||
if err := json.Unmarshal(raw, &object); err != nil || object == nil {
|
||||
return fmt.Errorf("%s is not a JSON object, and the mesh writes the trust of its registry "+
|
||||
"(%s) into it beside what is there, never over it (novox/hq ADR 0102). Make it an object "+
|
||||
"or remove it; nothing has been changed", runtimeConfig, strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("the runtime's configuration %s cannot be read: %w", runtimeConfig, err)
|
||||
}
|
||||
|
||||
asking, cancel := context.WithTimeout(ctx, o.Timeout)
|
||||
out, err := run(asking, "systemctl", "show", runtimeUnit, "--property=LoadState,ActiveState,MainPID")
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemd could not be asked about %s: %w\nThe registry's trust is taken by "+
|
||||
"reloading it; nothing has been changed", runtimeUnit, err)
|
||||
}
|
||||
props := map[string]string{}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if k, v, ok := strings.Cut(strings.TrimSpace(line), "="); ok {
|
||||
props[k] = v
|
||||
}
|
||||
}
|
||||
if props["LoadState"] != "loaded" || props["ActiveState"] != "active" {
|
||||
return fmt.Errorf("%s is %s and %s, and the registry's trust (%s) is taken by reloading it. "+
|
||||
"Genesis needs the runtime run by systemd as %s; nothing has been changed",
|
||||
runtimeUnit, orUnknown(props["LoadState"]), orUnknown(props["ActiveState"]),
|
||||
strings.Join(names, ", "), runtimeUnit)
|
||||
}
|
||||
if pid := props["MainPID"]; pid != "" && pid != "0" {
|
||||
if cmdline, err := os.ReadFile(procRoot + "/" + pid + "/cmdline"); err == nil {
|
||||
for _, arg := range strings.Split(string(cmdline), "\x00") {
|
||||
if strings.HasPrefix(arg, "--insecure-registry") {
|
||||
return fmt.Errorf("the runtime was started with %s, and dockerd refuses insecure registries "+
|
||||
"given both as a flag and in %s — so it would refuse the reload that makes it trust "+
|
||||
"%s. Move the flag's registries into the file and restart the runtime once; nothing "+
|
||||
"has been changed", arg, runtimeConfig, strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
say(" registry trust " + runtimeConfig + " can be written into and " + runtimeUnit + " reloaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func orUnknown(s string) string {
|
||||
if s == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user