Files
mesh-catalog/modules/mesh-build-image/cmd/mesh-build-image/probe.go
T
jschoubben d13f74df40 A node that cannot open /dev/net/tun takes no image build, and every node loads tun at boot and keeps its running kernel's modules
An image build node up 24 days had lost its running kernel's modules to a linux-lts upgrade, so tun
could not load and every RUN line failed in pasta. mesh-build-image now proves the tun device and,
whatever its look refuses, judges its node not eligible (capacity 0 with the reason), so the asks go
to a node that can build them. The kernel module loads tun at boot and declares kernel-modules-hook,
which keeps the running kernel's modules across an upgrade. Needs mesh-sdk fix/ace-tun (Puller.Eligible).
2026-10-28 12:44:58 +01:00

161 lines
5.8 KiB
Go

package main
// What the module proves about its own machine before it builds (novox/hq ADR 0487 §2, issue 1703a): buildah runs,
// the account holds subordinate user and group ranges (the node-engine's useradd allocates them for every `user`
// account, so nothing declares them; this reads them), a user namespace can be made, and the tun device opens (buildah
// makes a RUN line's network with pasta, which needs it; a build node on 2026-10-28 had the device node and no tun driver).
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// ProbeTimeout bounds each probe: they read the machine and fetch nothing.
const ProbeTimeout = 20 * time.Second
// TunDevice is the tun device pasta opens for a RUN line's network.
const TunDevice = "/dev/net/tun"
// Default subordinate id files.
const (
SubUIDFile = "/etc/subuid"
SubGIDFile = "/etc/subgid"
)
// buildahVersion runs `buildah --version` and answers the version, the third word of "buildah version 1.45.1 (...)".
func (m *Module) buildahVersion(ctx context.Context) (string, error) {
ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
defer cancel()
r := m.Run(ctx, m.env(), "buildah", "--version")
if r.Status != 0 || r.Err != "" {
return "", fmt.Errorf("`buildah --version` failed (is the module buildah assigned to this machine?): %s", firstLine(r.Err+"\n"+r.Stderr))
}
f := strings.Fields(r.Stdout)
if len(f) < 3 || f[0] != "buildah" || f[1] != "version" {
return "", fmt.Errorf("`buildah --version` answered %q", strings.TrimSpace(r.Stdout))
}
return f[2], nil
}
// userNamespace makes a user namespace as the account does for a build and runs `true` in it.
func (m *Module) userNamespace(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
defer cancel()
r := m.Run(ctx, m.env(), "buildah", append(m.storeFlags(), "unshare", "true")...)
if r.Status != 0 || r.Err != "" {
return fmt.Errorf("a user namespace cannot be made: %s", firstLine(r.Err+"\n"+r.Stderr))
}
return nil
}
// SubRange is how many subordinate ids file lists for the account (by name or by numeric uid): the sum of its lines'
// counts, zero when it has none.
func SubRange(file, account, uid string) (int, error) {
f, err := os.Open(file)
if err != nil {
return 0, err
}
defer f.Close()
total := 0
sc := bufio.NewScanner(f)
for sc.Scan() {
parts := strings.Split(strings.TrimSpace(sc.Text()), ":")
if len(parts) != 3 || (parts[0] != account && (uid == "" || parts[0] != uid)) {
continue
}
if n, err := strconv.Atoi(parts[2]); err == nil && n > 0 {
total += n
}
}
return total, sc.Err()
}
func (m *Module) subFiles() (uid, gid string) {
uid, gid = m.SubUIDFile, m.SubGIDFile
if uid == "" {
uid = SubUIDFile
}
if gid == "" {
gid = SubGIDFile
}
return
}
// ranges reads the account's subordinate ranges, or says which file lacks one.
func (m *Module) ranges() (uids, gids int, err error) {
uf, gf := m.subFiles()
for _, c := range []struct {
file string
n *int
}{{uf, &uids}, {gf, &gids}} {
n, rerr := SubRange(c.file, m.Account, m.UID)
if rerr != nil {
return 0, 0, fmt.Errorf("%s cannot be read: %v", c.file, rerr)
}
if n == 0 {
return 0, 0, fmt.Errorf("%s holds no subordinate range for the account %s: a build in a user namespace has no ids to map (the node-engine's useradd allocates them when it makes a user account)", c.file, m.Account)
}
*c.n = n
}
return uids, gids, nil
}
// storeFlags are buildah's global flags that put its image store and its runtime state in the module's store
// directory, outside the account's home.
func (m *Module) storeFlags() []string {
return []string{"--root", filepath.Join(m.Store, "root"), "--runroot", filepath.Join(m.Store, "run")}
}
// env is what buildah is given beyond the base environment: a runtime directory of its own inside the store, so
// nothing is read from the account's runtime directory, and no registry credential file anywhere (the module has none
// and REGISTRY_AUTH_FILE is never set).
//
// Every call makes the runtime directory first if it is not there (issue 1884): a directory removed after the last look
// is made again before buildah runs. A failure to make it is not lost: buildah then names the directory in its own
// error, and the next look refuses with it.
func (m *Module) env() []string {
_ = m.ensureRuntimeDir()
return append([]string{"XDG_RUNTIME_DIR=" + m.runtimeDir()}, m.threadEnv()...)
}
// runtimeDir is buildah's runtime directory, inside the store.
func (m *Module) runtimeDir() string { return filepath.Join(m.Store, "xdg") }
// ensureRuntimeDir makes the store and the runtime directory inside it, 0700 and under the module's own account (the
// process runs as it), when they are not there; it changes nothing that is.
func (m *Module) ensureRuntimeDir() error {
for _, d := range []string{m.Store, m.runtimeDir()} {
if err := os.MkdirAll(d, 0o700); err != nil {
return fmt.Errorf("buildah's runtime directory %s cannot be made: %w", m.runtimeDir(), err)
}
}
return nil
}
func (m *Module) tunPath() string {
if m.Tun != "" {
return m.Tun
}
return TunDevice
}
// tunDevice opens the tun device as the account does for a build, and says the node is not eligible when it cannot: a
// device node without its driver answers "no such device" (the tun kernel module is not loaded and cannot be, as when the
// running kernel's modules were removed by an upgrade), and every RUN line would fail in buildah's network setup.
func (m *Module) tunDevice() error {
f, err := os.OpenFile(m.tunPath(), os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("not eligible: %s cannot be opened (%v), so buildah cannot make a RUN line's network (pasta): "+
"the tun kernel module is not loaded here (the kernel module loads it at boot; a machine whose running kernel's "+
"modules are gone needs a reboot)", m.tunPath(), errors.Unwrap(err))
}
return f.Close()
}