GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package exec
  6
  7import (
  8	"fmt"
  9	"os"
 10	"path/filepath"
 11	"strings"
 12
 13	"github.com/samber/oops"
 14	executorLib "gitroot.dev/executor/lib"
 15	pluginLib "gitroot.dev/libs/golang/plugin/model"
 16	"gitroot.dev/server/configuration"
 17	"gitroot.dev/server/logger"
 18)
 19
 20type Bwrap struct {
 21	logger *logger.Logger
 22	conf   configuration.BwrapConf
 23}
 24
 25func NewBwrap(log *logger.Logger, conf configuration.BwrapConf) *Bwrap {
 26	mylog := log.NewSubLogger("bwrap")
 27	return &Bwrap{
 28		logger: mylog,
 29		conf:   conf,
 30	}
 31}
 32
 33func (e *Bwrap) name() string {
 34	return "bwrap"
 35}
 36
 37func (e *Bwrap) prepareTempDir(dir string) error {
 38	passwdContent := fmt.Sprintf("%s:x:%d:%d:%s:/home:/bin/sh\n", e.conf.User, e.conf.Uid, e.conf.Gid, e.conf.User)
 39	if err := os.MkdirAll(filepath.Join(dir, "etc"), 0755); err != nil {
 40		return oops.Wrapf(err, "can't mkdirall prepareFiles")
 41	}
 42	err := os.WriteFile(filepath.Join(dir, "etc", "passwd"), []byte(passwdContent), 0644)
 43	if err != nil {
 44		return oops.Wrapf(err, "can't write passwd file")
 45	}
 46
 47	groupContent := fmt.Sprintf("%s:x:%d:%s\n", e.conf.Group, e.conf.Gid, e.conf.User)
 48	err = os.WriteFile(filepath.Join(dir, "etc", "group"), []byte(groupContent), 0644)
 49	if err != nil {
 50		return oops.Wrapf(err, "can't write passwd file")
 51	}
 52
 53	if err := prepareJobRunner(dir); err != nil {
 54		return oops.Wrapf(err, "can't prepareJobRunner")
 55	}
 56
 57	return nil
 58}
 59
 60func (e *Bwrap) exec(dir string, exe pluginLib.Exec, projectPluginCacheDir string) (*pluginLib.ExecStatus, error) {
 61	args := []string{
 62		"--ro-bind", "/usr", "/usr",
 63		"--dir", "/tmp",
 64		"--dir", "/var",
 65		"--symlink", "../tmp", "var/tmp",
 66		"--proc", "/proc",
 67		"--dev", "/dev",
 68		"--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf",
 69		"--ro-bind", "/etc/ssl", "/etc/ssl",
 70		"--ro-bind", "/usr/lib/locale", "/usr/lib/locale",
 71		"--ro-bind", "/usr/share/i18n", "/usr/share/i18n",
 72		"--ro-bind", "/etc/alternatives", "/etc/alternatives",
 73		"--ro-bind", filepath.Join(dir, "etc", "passwd"), "/etc/passwd",
 74		"--ro-bind", filepath.Join(dir, "etc", "group"), "/etc/group",
 75		"--bind", filepath.Join(dir, APP_DIR), fmt.Sprintf("/%s", APP_DIR),
 76		"--bind", filepath.Join(dir, JOB_CONTEXT_DIR), fmt.Sprintf("/%s", JOB_CONTEXT_DIR),
 77		"--symlink", "usr/lib", "/lib",
 78		"--symlink", "usr/lib64", "/lib64",
 79		"--symlink", "usr/bin", "/bin",
 80		"--symlink", "usr/sbin", "/sbin",
 81		"--chdir", fmt.Sprintf("/%s", APP_DIR),
 82		"--unshare-all",
 83		"--share-net",
 84		"--die-with-parent",
 85		"--dir", fmt.Sprintf("/run/user/%d", e.conf.Uid),
 86		"--uid", fmt.Sprintf("%d", e.conf.Uid),
 87		"--gid", fmt.Sprintf("%d", e.conf.Gid),
 88		"--dir", "/home",
 89		"--setenv", "LANG", os.Getenv("LANG"),
 90		"--setenv", "LC_ALL", "C.UTF-8",
 91	}
 92
 93	for _, cache := range exe.Cache {
 94		cacheSource := filepath.Join(projectPluginCacheDir, CACHE_DIR, cache.Key)
 95		os.MkdirAll(cacheSource, os.ModePerm)
 96		if cache.ReadOnly {
 97			args = append(args, "--ro-bind", cacheSource, cache.Path)
 98		} else {
 99			args = append(args, "--bind", cacheSource, cache.Path)
100		}
101	}
102
103	myEnv := []string{
104		"HOME=/home",
105		"USER=nobody",
106		"LOGNAME=nobody",
107		"XDG_DATA_HOME=/home/.local/share",
108		"XDG_CONFIG_HOME=/home/.config",
109		"XDG_CACHE_HOME=/home/.cache",
110		fmt.Sprintf("LANG=%s", os.Getenv("LANG")),
111		"LC_ALL=C.UTF-8",
112		"PATH=/usr/bin:/bin",
113		fmt.Sprintf("XDG_RUNTIME_DIR=/run/user/%d", e.conf.Uid),
114	}
115	myEnv = append(myEnv, exe.Env...)
116
117	extraBinds := []string{}
118	for _, extern := range e.conf.RoBind {
119		intern := extern
120		if id := strings.Index(extern, ":"); id != -1 {
121			intern = extern[id+1:]
122			extern = extern[:id]
123		}
124		extraBinds = append(extraBinds, "--ro-bind", extern, intern)
125	}
126	for _, extern := range e.conf.Bind {
127		intern := extern
128		if id := strings.Index(extern, ":"); id != -1 {
129			intern = extern[id+1:]
130			extern = extern[:id]
131		}
132		extraBinds = append(extraBinds, "--bind", extern, intern)
133	}
134	args = append(args, extraBinds...)
135	exeWithEnv := pluginLib.Exec{ReportStats: exe.ReportStats, Cmds: exe.Cmds, Env: myEnv, Artifacts: []string{}, Cache: []pluginLib.Cache{}}
136	execs, err := cmdToExec(exeWithEnv, false)
137	if err != nil {
138		return nil, oops.Wrapf(err, "can't bwrap cmdToExec")
139	}
140	args = append(args, execs...)
141
142	subCmd := pluginLib.Exec{ReportStats: true, Cmds: []pluginLib.Cmd{{Cmd: "bwrap", Args: args}}, Env: exe.Env, Artifacts: exe.Artifacts, Cache: []pluginLib.Cache{}}
143	execStatus, err := executorLib.Start(filepath.Join(dir, APP_DIR), subCmd, false)
144	if err != nil {
145		return nil, oops.Wrapf(err, "can't bwrap start executorLib")
146	}
147	l := execStatus.CmdsLogs[0]
148	d, err := os.ReadFile(filepath.Join(dir, JOB_CONTEXT_DIR, l))
149	if err != nil {
150		return nil, oops.Wrapf(err, "can't bwrap ReadFile")
151	}
152	subExecStatus, err := parseJobLog(d)
153	if err != nil {
154		return execStatus, nil
155	}
156	for i, es := range subExecStatus.CmdsExec {
157		execStatus.CmdsExec = append(execStatus.CmdsExec, es)
158		execStatus.CmdsStatus = append(execStatus.CmdsStatus, subExecStatus.CmdsStatus[i])
159		execStatus.CmdsLogs = append(execStatus.CmdsLogs, subExecStatus.CmdsLogs[i])
160		execStatus.CmdsStats = append(execStatus.CmdsStats, subExecStatus.CmdsStats[i])
161	}
162	execStatus.Artifacts = append(execStatus.Artifacts, subExecStatus.Artifacts...)
163	return execStatus, nil
164}