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}