Give the integration level the node's installed Node toolchains read-only, so the Node build module's tests build offline (issue 1891, ADR 0533)
mesh-build-go binds /var/lib/mesh-build-node-cache/toolchain read-only into the integration level's one shut-off process when it is there; the unit level never. mesh-build-node opens its installed toolchains to other accounts (folders 0755, files 0644, writable by its owner alone; its cache 0711, so the npm cache beside them stays its own), and realMachine copies the toolchain of its own lock from there instead of npm ci from the registry, which the level cannot reach.
This commit is contained in:
@@ -68,6 +68,7 @@ func (m *Module) integrationShutOff(ctx context.Context, workspace string, packa
|
||||
}
|
||||
iso := m.isolation(workspace, false)
|
||||
iso.ReadOnly = append(iso.ReadOnly, exe) // the program alone, not the directory it lies in
|
||||
iso.ReadOnly = append(iso.ReadOnly, m.nodeToolchains()...)
|
||||
r := buildstep.Isolated(m.Run, iso)(ctx, nil, exe, levelArg, string(spec))
|
||||
var answer levelAnswer
|
||||
line := lastLine(r.Stdout)
|
||||
@@ -80,6 +81,25 @@ func (m *Module) integrationShutOff(ctx context.Context, workspace string, packa
|
||||
return answer.Result, nil
|
||||
}
|
||||
|
||||
// NodeToolchains is the Node build module's toolchains directory on this node: its cache directory (module.json
|
||||
// resource `cache`, mode 0711) joined with toolchain, where it installs typescript, esbuild, @novox/mesh-sdk and nats
|
||||
// from its own lock (ADR 0533 §1, §2; issue 1891).
|
||||
const NodeToolchains = "/var/lib/mesh-build-node-cache/toolchain"
|
||||
|
||||
// nodeToolchains is what the integration level is given of the Node build module's toolchains: their directory,
|
||||
// read-only, when it is there and this account may enter it, and nothing on a node where the Node build module has
|
||||
// installed none (ADR 0533 §1). The unit level is never given it.
|
||||
func (m *Module) nodeToolchains() []string {
|
||||
dir := m.NodeToolchains
|
||||
if dir == "" {
|
||||
dir = NodeToolchains
|
||||
}
|
||||
if _, err := os.ReadDir(dir); err != nil {
|
||||
return nil
|
||||
}
|
||||
return []string{dir}
|
||||
}
|
||||
|
||||
func lastLine(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
return strings.TrimSpace(lines[len(lines)-1])
|
||||
|
||||
@@ -62,6 +62,9 @@ type Module struct {
|
||||
Push func(ctx context.Context, registry, repository, digest string, body []byte) error
|
||||
// Executable is this program's path, which the integration level starts again shut off; os.Executable when empty.
|
||||
Executable string
|
||||
// NodeToolchains is where the Node build module keeps its installed toolchains, bound read-only into the
|
||||
// integration level (ADR 0533 §1); NodeToolchains when empty. A test points it elsewhere.
|
||||
NodeToolchains string
|
||||
// StartBus starts the throwaway bus of a run in dir and answers its address and what stops it (the module's own
|
||||
// program, StartBus in bus.go, when nil).
|
||||
StartBus func(ctx context.Context, dir string) (url string, stop func(), err error)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// integrationShutOffWith runs the integration level through the fake bubblewrap and answers what its one shut-off
|
||||
// process was given.
|
||||
func integrationShutOffWith(t *testing.T, toolchains string) shutOff {
|
||||
t.Helper()
|
||||
m, states, ws, _ := workspaceFor(t, "failing")
|
||||
m.Executable = "/opt/bundle/mesh-build-go"
|
||||
m.NodeToolchains = toolchains
|
||||
want := TestResult{Level: LevelIntegration, Passed: true, Tests: 1, Packages: []PackageResult{}, Store: "postgres 18"}
|
||||
var seen []shutOff
|
||||
m.Run = throughBubblewrap(t, func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
switch {
|
||||
case name == "postgres" && len(args) == 1 && args[0] == "--version":
|
||||
return forgekey.Ran{Stdout: "postgres (PostgreSQL) 18.6\n"}
|
||||
case name == m.Executable && len(args) == 2 && args[0] == levelArg:
|
||||
raw, _ := json.Marshal(levelAnswer{Result: &want})
|
||||
return forgekey.Ran{Stdout: string(raw) + "\n"}
|
||||
}
|
||||
return forgekey.Ran{Status: 1, Err: "not expected here"}
|
||||
}, &seen)
|
||||
run := stepTool(t, m, "build_go_test")
|
||||
if _, err := run(map[string]any{"workspace": ws, "level": LevelIntegration}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st := states.waitEnd(t); st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
if len(seen) != 1 {
|
||||
t.Fatalf("shut off %d processes, want one: %v", len(seen), seen)
|
||||
}
|
||||
checkShutOff(t, m, seen[0], false)
|
||||
return seen[0]
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §1): the integration level is given the Node build module's installed toolchains, read-only, so the
|
||||
// Node build module's own integration tests build with the real tsc and esbuild and reach no registry.
|
||||
func TestTheIntegrationLevelIsGivenTheNodeToolchainsReadOnly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := integrationShutOffWith(t, dir)
|
||||
if !slices.Contains(s.roBinds, dir) {
|
||||
t.Errorf("the node toolchains %s are not bound read-only: %v", dir, s.roBinds)
|
||||
}
|
||||
if slices.Contains(s.binds, dir) || slices.Contains(s.overlays, dir) {
|
||||
t.Errorf("the node toolchains %s may be written: binds %v, overlays %v", dir, s.binds, s.overlays)
|
||||
}
|
||||
if slices.Contains(s.roBinds, filepath.Dir(dir)) {
|
||||
t.Errorf("the whole cache %s is bound, not its toolchains alone: %v", filepath.Dir(dir), s.roBinds)
|
||||
}
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §1): a node where the Node build module has installed no toolchain runs the level as before, with
|
||||
// nothing more bound (bubblewrap binds only what is there).
|
||||
func TestANodeWithoutNodeToolchainsRunsTheLevelWithNothingMoreBound(t *testing.T) {
|
||||
missing := filepath.Join(t.TempDir(), "none")
|
||||
s := integrationShutOffWith(t, missing)
|
||||
if slices.Contains(s.roBinds, missing) {
|
||||
t.Errorf("a missing %s is bound: %v", missing, s.roBinds)
|
||||
}
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §1): the unit level is not given them.
|
||||
func TestTheUnitLevelIsNotGivenTheNodeToolchains(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "failing")
|
||||
dir := t.TempDir()
|
||||
m.NodeToolchains = dir
|
||||
var seen []shutOff
|
||||
m.Run = throughBubblewrap(t, m.Run, &seen)
|
||||
if _, st := runUnit(t, m, states, ws); st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
for _, s := range seen {
|
||||
if slices.Contains(s.roBinds, dir) || slices.Contains(s.binds, dir) {
|
||||
t.Errorf("%s was given the node toolchains", s.command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §1, §2): the place bound is the Node build module's own: its manifest's cache directory joined with
|
||||
// toolchain, and that cache directory lets other accounts enter it without listing it (0711).
|
||||
func TestTheNodeToolchainsAreTheNodeBuildModulesCache(t *testing.T) {
|
||||
if _, err := catalogue.ReadTreeManifest(catalogueRoot, filepath.Join(catalogueRoot, "modules", "mesh-build-node")); err != nil {
|
||||
t.Fatalf("mesh-sdk's validator refuses mesh-build-node's manifest: %v", err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(catalogueRoot, "modules", "mesh-build-node", "module.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var man struct {
|
||||
Resources []struct {
|
||||
ID, Path, Mode string
|
||||
} `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &man); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, r := range man.Resources {
|
||||
if r.ID != "cache" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if want := filepath.Join(r.Path, "toolchain"); NodeToolchains != want {
|
||||
t.Errorf("NodeToolchains = %s, want %s (mesh-build-node's cache)", NodeToolchains, want)
|
||||
}
|
||||
mode, err := strconv.ParseUint(r.Mode, 8, 32)
|
||||
if err != nil || mode&0o001 == 0 || mode&0o004 != 0 {
|
||||
t.Errorf("mesh-build-node's cache has mode %q; want others to enter it and not list it (0711)", r.Mode)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("mesh-build-node's manifest declares no cache directory")
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -13,8 +16,11 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// realMachine installs the real toolchain from the module's own lock (npm ci --ignore-scripts, from the mesh's
|
||||
// registry for @novox, which reads without a credential) and makes two workspaces holding one fixture bundle.
|
||||
// realMachine gives the tests the real toolchain of the module's own lock and makes two workspaces holding one fixture
|
||||
// bundle. The toolchain is the one this node's installed Node build module keeps (ADR 0533 §1): the integration level
|
||||
// is given its toolchains read-only and no network, so it is copied into the test's own cache from there. Only where
|
||||
// no toolchain is installed (a desk) is it installed with `npm ci --ignore-scripts` from the mesh's registry, as the
|
||||
// module does, which needs the network.
|
||||
func realMachine(t *testing.T) (*Module, string) {
|
||||
t.Helper()
|
||||
for _, p := range []string{"node", "npm", "bwrap"} { // bwrap: tsc, esbuild and the scripts run shut off (1309)
|
||||
@@ -40,8 +46,11 @@ func realMachine(t *testing.T) (*Module, string) {
|
||||
return Registry{URL: "https://git.novox.be/api/packages/novox/npm/"}, nil
|
||||
}}}
|
||||
withStore(t, m)
|
||||
if from := installedToolchain(t, filepath.Join(CacheDir, "toolchain"), filepath.Join(home, "toolchain", "package-lock.json")); from != "" {
|
||||
copyToolchain(t, from, m.Toolchain.Paths.Dir(m.Recipe.Version()))
|
||||
}
|
||||
if err := m.Toolchain.Ensure(context.Background(), m.Recipe.Version()); err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("no toolchain of this lock is installed under %s, and installing it from the registry failed: %v", filepath.Join(CacheDir, "toolchain"), err)
|
||||
}
|
||||
for _, id := range []string{"w1", "w2"} {
|
||||
for p, body := range map[string]string{
|
||||
@@ -115,3 +124,61 @@ func TestAPackageWhoseTestsImportItsBuildPassesTheUnitLevel(t *testing.T) {
|
||||
t.Errorf("result = %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// installedToolchain is the toolchain directory under toolchains that this node's Node build module installed from
|
||||
// the lock at lockPath, byte for byte; "" when there is none (or the directory cannot be read: no Node build module
|
||||
// here). A toolchain of another lock is never taken: the tests prove the lock they carry.
|
||||
func installedToolchain(t *testing.T, toolchains, lockPath string) string {
|
||||
t.Helper()
|
||||
want, err := os.ReadFile(lockPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err := os.ReadDir(toolchains)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, e := range entries {
|
||||
dir := filepath.Join(toolchains, e.Name())
|
||||
if _, err := os.Stat(filepath.Join(dir, installedMark)); err != nil {
|
||||
continue
|
||||
}
|
||||
if got, err := os.ReadFile(filepath.Join(dir, "package-lock.json")); err == nil && bytes.Equal(got, want) {
|
||||
t.Logf("the toolchain is this node's installed %s", dir)
|
||||
return dir
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// copyToolchain copies the installed toolchain from into to whole, files and folders with their modes, so the test's
|
||||
// steps run behind a toolchain of their own and the installed one is only read.
|
||||
func copyToolchain(t *testing.T, from, to string) {
|
||||
t.Helper()
|
||||
err := filepath.WalkDir(from, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, _ := filepath.Rel(from, p)
|
||||
dst := filepath.Join(to, rel)
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case d.IsDir():
|
||||
return os.MkdirAll(dst, 0o755)
|
||||
case info.Mode().IsRegular():
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, raw, info.Mode().Perm()|0o600)
|
||||
default:
|
||||
return fmt.Errorf("%s is neither a file nor a folder", p)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("copying the installed toolchain %s: %v", from, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -53,14 +54,17 @@ func (t Toolchain) Present(version string) bool {
|
||||
// are copied in, and `npm ci --ignore-scripts` runs once, the mesh's registry named for the @novox scope. A directory
|
||||
// an earlier install left unfinished is removed first.
|
||||
func (t Toolchain) Ensure(ctx context.Context, version string) error {
|
||||
if t.Present(version) {
|
||||
return nil
|
||||
}
|
||||
dir := t.Paths.Dir(version)
|
||||
if t.Present(version) {
|
||||
if opened(dir) {
|
||||
return nil
|
||||
}
|
||||
return t.open(dir)
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range []string{"package.json", "package-lock.json"} {
|
||||
@@ -87,7 +91,55 @@ func (t Toolchain) Ensure(ctx context.Context, version string) error {
|
||||
return fmt.Errorf("npm ci --ignore-scripts for the toolchain %s failed (status %d %s): %s",
|
||||
version, ran.Status, ran.Err, tail(ran.Stderr))
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dir, installedMark), []byte(version+"\n"), 0o600)
|
||||
if err := os.WriteFile(filepath.Join(dir, installedMark), []byte(version+"\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return t.open(dir)
|
||||
}
|
||||
|
||||
// opened says whether the toolchain directory dir was opened to other accounts: its installed mark, opened last, is
|
||||
// readable by others.
|
||||
func opened(dir string) bool {
|
||||
st, err := os.Stat(filepath.Join(dir, installedMark))
|
||||
return err == nil && st.Mode().Perm()&0o004 != 0
|
||||
}
|
||||
|
||||
// open lets every account read the toolchain directory dir and nobody but its owner write it (ADR 0533 §2): the
|
||||
// toolchains folder and every folder beneath dir 0755, every file 0644 and every program 0755, whatever umask npm ran
|
||||
// under; the installed mark last, so a toolchain opened only in part is opened again. The other build module's
|
||||
// integration level reads it through the cache directory, which others may enter and not list (0711); the npm cache
|
||||
// beside it stays the owner's.
|
||||
func (t Toolchain) open(dir string) error {
|
||||
if err := os.Chmod(filepath.Join(t.Paths.Cache, "toolchain"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
mark := filepath.Join(dir, installedMark)
|
||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p == mark {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch mode := info.Mode(); {
|
||||
case mode.IsDir():
|
||||
return os.Chmod(p, 0o755)
|
||||
case mode.IsRegular() && mode.Perm()&0o100 != 0:
|
||||
return os.Chmod(p, 0o755)
|
||||
case mode.IsRegular():
|
||||
return os.Chmod(p, 0o644)
|
||||
default:
|
||||
return fmt.Errorf("%s is neither a file nor a folder, and a toolchain holds only those", p)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("the toolchain %s cannot be opened to the integration level: %w", dir, err)
|
||||
}
|
||||
return os.Chmod(mark, 0o644)
|
||||
}
|
||||
|
||||
func tail(s string) string {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// npmAsUmask077 installs as npm does under a umask of 077: node_modules with a package whose folders are 0700, its
|
||||
// file 0600 and its program 0700.
|
||||
func npmAsUmask077(f *fakeNpm) forgekey.Runner {
|
||||
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
ran := f.run(ctx, env, name, args...)
|
||||
for i, a := range args {
|
||||
if a == "--prefix" {
|
||||
pkg := filepath.Join(args[i+1], "node_modules", "typescript", "bin")
|
||||
os.MkdirAll(pkg, 0o700)
|
||||
os.WriteFile(filepath.Join(args[i+1], "node_modules", "typescript", "package.json"), []byte("{}"), 0o600)
|
||||
os.WriteFile(filepath.Join(pkg, "tsc"), []byte("#!/bin/sh\n"), 0o700)
|
||||
}
|
||||
}
|
||||
return ran
|
||||
}
|
||||
}
|
||||
|
||||
// checkOpen fails unless every folder from the cache's toolchain down lets others enter and read it and every file lets
|
||||
// others read it, a program others run, and nothing lets anyone but the owner write.
|
||||
func checkOpen(t *testing.T, tc Toolchain) {
|
||||
t.Helper()
|
||||
root := filepath.Join(tc.Paths.Cache, "toolchain")
|
||||
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := info.Mode().Perm()
|
||||
switch {
|
||||
case mode&0o022 != 0:
|
||||
t.Errorf("%s is %o: writable by others than its owner", p, mode)
|
||||
case d.IsDir() && mode&0o005 != 0o005:
|
||||
t.Errorf("%s is %o: others may not enter and read it", p, mode)
|
||||
case !d.IsDir() && mode&0o004 == 0:
|
||||
t.Errorf("%s is %o: others may not read it", p, mode)
|
||||
case !d.IsDir() && mode&0o100 != 0 && mode&0o001 == 0:
|
||||
t.Errorf("%s is %o: a program others may not run", p, mode)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §2): a toolchain the module installs can be read by the other build module's integration level:
|
||||
// its folders 0755 and its files 0644 (programs 0755), whatever umask npm ran under, and writable by its owner alone.
|
||||
func TestAnInstalledToolchainIsReadableByOthersAndWritableByItsOwnerAlone(t *testing.T) {
|
||||
fake := &fakeNpm{}
|
||||
tc := toolchainIn(t, fake)
|
||||
tc.Run = npmAsUmask077(fake)
|
||||
if err := tc.Ensure(context.Background(), "v1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkOpen(t, tc)
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §2): a toolchain installed before this release, shut to its owner, is opened by the next Ensure
|
||||
// without installing it again.
|
||||
func TestAToolchainInstalledShutIsOpenedWithoutAnInstall(t *testing.T) {
|
||||
fake := &fakeNpm{}
|
||||
tc := toolchainIn(t, fake)
|
||||
dir := tc.Paths.Dir("v1")
|
||||
os.MkdirAll(filepath.Join(dir, "node_modules", "esbuild", "bin"), 0o700)
|
||||
os.WriteFile(filepath.Join(dir, "node_modules", "esbuild", "bin", "esbuild"), []byte("x"), 0o700)
|
||||
os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte("{}"), 0o600)
|
||||
os.WriteFile(filepath.Join(dir, installedMark), []byte("v1\n"), 0o600)
|
||||
os.Chmod(filepath.Join(tc.Paths.Cache, "toolchain"), 0o700)
|
||||
if err := tc.Ensure(context.Background(), "v1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fake.calls) != 0 {
|
||||
t.Errorf("npm ran %d times for a toolchain already there", len(fake.calls))
|
||||
}
|
||||
checkOpen(t, tc)
|
||||
}
|
||||
|
||||
// 1891 (ADR 0533 §2): the cache directory lets other accounts enter it and not list it, so the toolchains beneath it
|
||||
// can be reached by their path and the npm cache beside them (0700) cannot.
|
||||
func TestTheCacheDirectoryIsEnteredByOthersNotListed(t *testing.T) {
|
||||
r := resource(t, readManifest(t), "cache")
|
||||
if r["mode"] != "0711" {
|
||||
t.Errorf("the cache directory's mode is %v, want 0711", r["mode"])
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
"id": "cache",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh-build-node-cache",
|
||||
"mode": "0700",
|
||||
"mode": "0711",
|
||||
"owner": "mesh-build-node",
|
||||
"unknown": "program"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user