forked from loft-sh/devpod
-
Notifications
You must be signed in to change notification settings - Fork 0
/
agent.go
380 lines (315 loc) · 10.6 KB
/
agent.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/loft-sh/devpod/pkg/command"
"github.com/loft-sh/devpod/pkg/compress"
provider2 "github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/pkg/version"
"github.com/loft-sh/log"
perrors "github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const DefaultInactivityTimeout = time.Minute * 20
const ContainerDevPodHelperLocation = "/usr/local/bin/devpod"
const RemoteDevPodHelperLocation = "/tmp/devpod"
const ContainerActivityFile = "/tmp/devpod.activity"
const defaultAgentDownloadURL = "https://github.com/loft-sh/devpod/releases/download/"
const EnvDevPodAgentURL = "DEVPOD_AGENT_URL"
const WorkspaceBusyFile = "workspace.lock"
func DefaultAgentDownloadURL() string {
devPodAgentURL := os.Getenv(EnvDevPodAgentURL)
if devPodAgentURL != "" {
return strings.TrimSuffix(devPodAgentURL, "/") + "/"
}
if version.GetVersion() == version.DevVersion {
return "https://github.com/loft-sh/devpod/releases/latest/download/"
}
return defaultAgentDownloadURL + version.GetVersion()
}
func DecodeContainerWorkspaceInfo(workspaceInfoRaw string) (*provider2.ContainerWorkspaceInfo, string, error) {
decoded, err := compress.Decompress(workspaceInfoRaw)
if err != nil {
return nil, "", perrors.Wrap(err, "decode workspace info")
}
workspaceInfo := &provider2.ContainerWorkspaceInfo{}
err = json.Unmarshal([]byte(decoded), workspaceInfo)
if err != nil {
return nil, "", perrors.Wrap(err, "parse workspace info")
}
return workspaceInfo, decoded, nil
}
func DecodeWorkspaceInfo(workspaceInfoRaw string) (*provider2.AgentWorkspaceInfo, string, error) {
decoded, err := compress.Decompress(workspaceInfoRaw)
if err != nil {
return nil, "", perrors.Wrap(err, "decode workspace info")
}
workspaceInfo := &provider2.AgentWorkspaceInfo{}
err = json.Unmarshal([]byte(decoded), workspaceInfo)
if err != nil {
return nil, "", perrors.Wrap(err, "parse workspace info")
}
return workspaceInfo, decoded, nil
}
func readAgentWorkspaceInfo(agentFolder, context, id string) (*provider2.AgentWorkspaceInfo, error) {
// get workspace folder
workspaceDir, err := GetAgentWorkspaceDir(agentFolder, context, id)
if err != nil {
return nil, err
}
// parse agent workspace info
return ParseAgentWorkspaceInfo(filepath.Join(workspaceDir, provider2.WorkspaceConfigFile))
}
func ParseAgentWorkspaceInfo(workspaceConfigFile string) (*provider2.AgentWorkspaceInfo, error) {
// read workspace config
out, err := os.ReadFile(workspaceConfigFile)
if err != nil {
return nil, err
}
// json unmarshal
workspaceInfo := &provider2.AgentWorkspaceInfo{}
err = json.Unmarshal(out, workspaceInfo)
if err != nil {
return nil, perrors.Wrap(err, "parse workspace info")
}
workspaceInfo.Origin = filepath.Dir(workspaceConfigFile)
return workspaceInfo, nil
}
func ReadAgentWorkspaceInfo(agentFolder, context, id string, log log.Logger) (bool, *provider2.AgentWorkspaceInfo, error) {
workspaceInfo, err := readAgentWorkspaceInfo(agentFolder, context, id)
if err != nil && !(errors.Is(err, ErrFindAgentHomeFolder) || errors.Is(err, os.ErrPermission)) {
return false, nil, err
}
// check if we need to become root
shouldExit, err := rerunAsRoot(workspaceInfo, log)
if err != nil {
return false, nil, perrors.Wrap(err, "rerun as root")
} else if shouldExit {
return true, nil, nil
} else if workspaceInfo == nil {
return false, nil, ErrFindAgentHomeFolder
}
return false, workspaceInfo, nil
}
func WorkspaceInfo(workspaceInfoEncoded string, log log.Logger) (bool, *provider2.AgentWorkspaceInfo, error) {
return decodeWorkspaceInfoAndWrite(workspaceInfoEncoded, false, nil, log)
}
func WriteWorkspaceInfo(workspaceInfoEncoded string, log log.Logger) (bool, *provider2.AgentWorkspaceInfo, error) {
return WriteWorkspaceInfoAndDeleteOld(workspaceInfoEncoded, nil, log)
}
func WriteWorkspaceInfoAndDeleteOld(workspaceInfoEncoded string, deleteWorkspace func(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger) error, log log.Logger) (bool, *provider2.AgentWorkspaceInfo, error) {
return decodeWorkspaceInfoAndWrite(workspaceInfoEncoded, true, deleteWorkspace, log)
}
func decodeWorkspaceInfoAndWrite(
workspaceInfoEncoded string,
writeInfo bool,
deleteWorkspace func(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger) error,
log log.Logger,
) (bool, *provider2.AgentWorkspaceInfo, error) {
workspaceInfo, _, err := DecodeWorkspaceInfo(workspaceInfoEncoded)
if err != nil {
return false, nil, err
}
// check if we need to become root
shouldExit, err := rerunAsRoot(workspaceInfo, log)
if err != nil {
return false, nil, fmt.Errorf("rerun as root: %w", err)
} else if shouldExit {
return true, nil, nil
}
// write to workspace folder
workspaceDir, err := CreateAgentWorkspaceDir(workspaceInfo.Agent.DataPath, workspaceInfo.Workspace.Context, workspaceInfo.Workspace.ID)
if err != nil {
return false, nil, err
}
log.Debugf("Use %s as workspace dir", workspaceDir)
// check if workspace config already exists
workspaceConfig := filepath.Join(workspaceDir, provider2.WorkspaceConfigFile)
if deleteWorkspace != nil {
oldWorkspaceInfo, _ := ParseAgentWorkspaceInfo(workspaceConfig)
if oldWorkspaceInfo != nil && oldWorkspaceInfo.Workspace.UID != workspaceInfo.Workspace.UID {
// delete the old workspace
log.Infof("Delete old workspace '%s'", oldWorkspaceInfo.Workspace.ID)
err = deleteWorkspace(oldWorkspaceInfo, log)
if err != nil {
return false, nil, perrors.Wrap(err, "delete old workspace")
}
// recreate workspace folder again
workspaceDir, err = CreateAgentWorkspaceDir(workspaceInfo.Agent.DataPath, workspaceInfo.Workspace.Context, workspaceInfo.Workspace.ID)
if err != nil {
return false, nil, err
}
}
}
// check content folder
if workspaceInfo.Workspace.Source.LocalFolder != "" {
_, err = os.Stat(workspaceInfo.WorkspaceOrigin)
if err == nil {
workspaceInfo.ContentFolder = workspaceInfo.Workspace.Source.LocalFolder
}
}
// set content folder
if workspaceInfo.ContentFolder == "" {
workspaceInfo.ContentFolder = GetAgentWorkspaceContentDir(workspaceDir)
}
// write workspace info
if writeInfo {
err = writeWorkspaceInfo(workspaceConfig, workspaceInfo)
if err != nil {
return false, nil, err
}
}
workspaceInfo.Origin = workspaceDir
return false, workspaceInfo, nil
}
func CreateWorkspaceBusyFile(folder string) {
filePath := filepath.Join(folder, WorkspaceBusyFile)
_, err := os.Stat(filePath)
if err == nil {
return
}
_ = os.WriteFile(filePath, nil, 0600)
}
func HasWorkspaceBusyFile(folder string) bool {
filePath := filepath.Join(folder, WorkspaceBusyFile)
_, err := os.Stat(filePath)
return err == nil
}
func DeleteWorkspaceBusyFile(folder string) {
_ = os.Remove(filepath.Join(folder, WorkspaceBusyFile))
}
func writeWorkspaceInfo(file string, workspaceInfo *provider2.AgentWorkspaceInfo) error {
// copy workspace info
cloned := provider2.CloneAgentWorkspaceInfo(workspaceInfo)
// never save cli options
cloned.CLIOptions = provider2.CLIOptions{}
// encode workspace info
encoded, err := json.Marshal(workspaceInfo)
if err != nil {
return err
}
// write workspace config
err = os.WriteFile(file, encoded, 0600)
if err != nil {
return fmt.Errorf("write workspace config file: %w", err)
}
return nil
}
func rerunAsRoot(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger) (bool, error) {
// check if root is required
if runtime.GOOS != "linux" || os.Getuid() == 0 || (workspaceInfo != nil && workspaceInfo.Agent.Local == "true") {
return false, nil
}
// check if we can reach docker with no problems
dockerRootRequired := false
if workspaceInfo != nil && (workspaceInfo.Agent.Driver == "" || workspaceInfo.Agent.Driver == provider2.DockerDriver) {
var err error
dockerRootRequired, err = dockerReachable(workspaceInfo.Agent.Docker.Path, workspaceInfo.Agent.Docker.Env)
if err != nil {
log.Debugf("Error trying to reach docker daemon: %v", err)
dockerRootRequired = true
}
}
// check if daemon needs to be installed
agentRootRequired := false
if workspaceInfo == nil || len(workspaceInfo.Agent.Exec.Shutdown) > 0 {
agentRootRequired = true
}
// check if root required
if !dockerRootRequired && !agentRootRequired {
log.Debugf("No root required, because neither docker nor agent daemon needs to be installed")
return false, nil
}
// execute ourself as root
binary, err := os.Executable()
if err != nil {
return false, err
}
// call ourself
args := []string{"--preserve-env", binary}
args = append(args, os.Args[1:]...)
log.Debugf("Rerun as root: %s", strings.Join(args, " "))
cmd := exec.Command("sudo", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return false, err
}
return true, nil
}
type Exec func(ctx context.Context, user string, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error
func Tunnel(
ctx context.Context,
exec Exec,
user string,
stdin io.Reader,
stdout io.Writer,
stderr io.Writer,
log log.Logger,
timeout time.Duration,
) error {
// inject agent
err := InjectAgent(ctx, func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
return exec(ctx, "root", command, stdin, stdout, stderr)
}, false, ContainerDevPodHelperLocation, DefaultAgentDownloadURL(), false, log, timeout)
if err != nil {
return err
}
// build command
command := fmt.Sprintf("'%s' helper ssh-server --stdio", ContainerDevPodHelperLocation)
if log.GetLevel() == logrus.DebugLevel {
command += " --debug"
}
if user == "" {
user = "root"
}
// create tunnel
err = exec(ctx, user, command, stdin, stdout, stderr)
if err != nil {
return err
}
return nil
}
func dockerReachable(dockerOverride string, envs map[string]string) (bool, error) {
docker := "docker"
if dockerOverride != "" {
docker = dockerOverride
}
if !command.Exists(docker) {
// if docker is overridden, we assume that there is an error as we don't know how to install the command provided
if dockerOverride != "" {
return false, fmt.Errorf("docker command '%s' not found", dockerOverride)
}
// we need root to install docker
return true, nil
}
cmd := exec.Command(docker, "ps")
if len(envs) > 0 {
newEnvs := os.Environ()
for k, v := range envs {
newEnvs = append(newEnvs, k+"="+v)
}
cmd.Env = newEnvs
}
_, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(err.Error(), "permission denied") {
if dockerOverride == "" {
return true, nil
}
}
return false, perrors.Wrapf(err, "%s ps", docker)
}
return false, nil
}