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).
161 lines
5.8 KiB
Go
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()
|
|
}
|