From b53b0e7e2cc436f11bc9190a937167299f532d9e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 23:39:51 +0200 Subject: [PATCH] Make buildah's runtime directory before every buildah run, so a machine without it is not unhealthy forever (issue 1884) The look made /xdg only after `buildah --version`, which refuses first when its XDG_RUNTIME_DIR is missing: the directory was never made on ace. --- .../cmd/mesh-build-image/module.go | 12 ++- .../cmd/mesh-build-image/probe.go | 21 ++++- .../cmd/mesh-build-image/runtimedir_test.go | 83 +++++++++++++++++++ 3 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 modules/mesh-build-image/cmd/mesh-build-image/runtimedir_test.go diff --git a/modules/mesh-build-image/cmd/mesh-build-image/module.go b/modules/mesh-build-image/cmd/mesh-build-image/module.go index 9af9cfbc..c6d869c6 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/module.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/module.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/http" - "os" - "path/filepath" "time" "git.novox.be/novox/mesh-sdk/go/buildstep" @@ -60,6 +58,11 @@ type Look struct { // missing is a *buildstep.Refusal naming it, so a step ends refused and the health says the same. func (m *Module) look(ctx context.Context) (Look, error) { l := Look{Account: m.Account} + // The runtime directory comes first: buildah refuses before anything else when its XDG_RUNTIME_DIR is not there + // (issue 1884), so a look that made it only after `buildah --version` never made it at all. + if err := m.ensureRuntimeDir(); err != nil { + return l, &buildstep.Refusal{Text: err.Error()} + } var err error if l.Buildah, err = m.buildahVersion(ctx); err != nil { return l, &buildstep.Refusal{Text: err.Error()} @@ -67,11 +70,6 @@ func (m *Module) look(ctx context.Context) (Look, error) { if l.UIDs, l.GIDs, err = m.ranges(); err != nil { return l, &buildstep.Refusal{Text: err.Error(), Have: l.Buildah} } - for _, d := range []string{m.Store, filepath.Join(m.Store, "xdg")} { - if err := os.MkdirAll(d, 0o700); err != nil { - return l, err - } - } if err := m.userNamespace(ctx); err != nil { return l, &buildstep.Refusal{Text: err.Error(), Have: l.Buildah} } diff --git a/modules/mesh-build-image/cmd/mesh-build-image/probe.go b/modules/mesh-build-image/cmd/mesh-build-image/probe.go index 3b2a587d..e96c2212 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/probe.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/probe.go @@ -111,6 +111,25 @@ func (m *Module) storeFlags() []string { // 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 { - return []string{"XDG_RUNTIME_DIR=" + filepath.Join(m.Store, "xdg")} + _ = m.ensureRuntimeDir() + return []string{"XDG_RUNTIME_DIR=" + m.runtimeDir()} +} + +// 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 } diff --git a/modules/mesh-build-image/cmd/mesh-build-image/runtimedir_test.go b/modules/mesh-build-image/cmd/mesh-build-image/runtimedir_test.go new file mode 100644 index 00000000..d2445774 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/runtimedir_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" + "git.novox.be/novox/mesh-sdk/go/health" +) + +// buildahLike refuses as buildah does when the XDG_RUNTIME_DIR it is given is not there (seen on ace, issue 1884): +// "lstat : no such file or directory", before it does anything else. +func buildahLike(t *testing.T, run forgekey.Runner, seen *[]string) forgekey.Runner { + t.Helper() + return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { + if name == "buildah" { + for _, e := range env { + dir, ok := strings.CutPrefix(e, "XDG_RUNTIME_DIR=") + if !ok { + continue + } + if _, err := os.Lstat(dir); err != nil { + *seen = append(*seen, "missing") + return forgekey.Ran{Status: 125, Stderr: "Error: lstat " + dir + ": no such file or directory\n"} + } + *seen = append(*seen, "present") + } + } + return run(ctx, env, name, args...) + } +} + +// 1884/AC1: given a store directory that exists without its runtime directory, when the health is looked at, then the +// module makes the runtime directory (0700) before it runs buildah, and the health is healthy. +func TestHealthMakesTheRuntimeDirectoryBeforeBuildah(t *testing.T) { + f := newFake(t) + m := moduleWith(t, f, newStates()) + var seen []string + m.Run = buildahLike(t, m.Run, &seen) + xdg := filepath.Join(m.Store, "xdg") + if _, err := os.Stat(xdg); !os.IsNotExist(err) { + t.Fatalf("the test needs a store without its runtime directory: %v", err) + } + a := m.Health(context.Background()) + if a.State != health.StateHealthy || !a.Healthy { + t.Fatalf("health = %+v, want healthy", a) + } + st, err := os.Stat(xdg) + if err != nil { + t.Fatalf("the runtime directory was not made: %v", err) + } + if st.Mode().Perm() != 0o700 || !st.IsDir() { + t.Errorf("the runtime directory is %v, want a directory 0700", st.Mode()) + } + for _, s := range seen { + if s != "present" { + t.Fatalf("buildah ran without its runtime directory: %v", seen) + } + } +} + +// 1884/AC2: given a module whose runtime directory was removed after its last look, when a build step runs buildah, then +// the runtime directory is made again before buildah runs. +func TestEveryBuildahRunMakesTheRuntimeDirectoryAgain(t *testing.T) { + f := newFake(t) + m := moduleWith(t, f, newStates()) + var seen []string + m.Run = buildahLike(t, m.Run, &seen) + if err := m.Check(context.Background()); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(filepath.Join(m.Store, "xdg")); err != nil { + t.Fatal(err) + } + seen = nil + m.remove("localhost/x:y") + if len(seen) != 1 || seen[0] != "present" { + t.Fatalf("buildah's runs saw their runtime directory %v, want [present]", seen) + } +} -- 2.54.0