PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/components/ws-daemon/pkg/content/initializer.go

https://gitlab.com/geropl/gitpod
Go | 442 lines | 340 code | 67 blank | 35 comment | 96 complexity | e9bfc02c9b2bc4a860872e0116e0ddea MD5 | raw file
  1. // Copyright (c) 2020 Gitpod GmbH. All rights reserved.
  2. // Licensed under the GNU Affero General Public License (AGPL).
  3. // See License-AGPL.txt in the project root for license information.
  4. package content
  5. import (
  6. "bytes"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "io/ioutil"
  11. "net/http"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strings"
  16. "syscall"
  17. "time"
  18. "github.com/google/uuid"
  19. "github.com/opencontainers/runc/libcontainer/specconv"
  20. "github.com/opencontainers/runtime-spec/specs-go"
  21. "github.com/opentracing/opentracing-go"
  22. "github.com/sirupsen/logrus"
  23. "golang.org/x/xerrors"
  24. "google.golang.org/protobuf/proto"
  25. "github.com/gitpod-io/gitpod/common-go/log"
  26. "github.com/gitpod-io/gitpod/common-go/tracing"
  27. csapi "github.com/gitpod-io/gitpod/content-service/api"
  28. "github.com/gitpod-io/gitpod/content-service/pkg/archive"
  29. wsinit "github.com/gitpod-io/gitpod/content-service/pkg/initializer"
  30. "github.com/gitpod-io/gitpod/content-service/pkg/storage"
  31. )
  32. // RunInitializerOpts configure RunInitializer
  33. type RunInitializerOpts struct {
  34. // Command is the path to the initializer executable we'll run
  35. Command string
  36. // Args is a set of additional arguments to pass to the initializer executable
  37. Args []string
  38. // Options to use on untar
  39. IdMappings []archive.IDMapping
  40. UID uint32
  41. GID uint32
  42. OWI OWI
  43. }
  44. type OWI struct {
  45. Owner string
  46. WorkspaceID string
  47. InstanceID string
  48. }
  49. func (o OWI) Fields() map[string]interface{} {
  50. return log.OWI(o.Owner, o.WorkspaceID, o.InstanceID)
  51. }
  52. // errors to be tested with errors.Is
  53. var (
  54. // cannot find snapshot
  55. errCannotFindSnapshot = errors.New("cannot find snapshot")
  56. )
  57. func collectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps storage.PresignedAccess, workspaceOwner string, initializer *csapi.WorkspaceInitializer) (rc map[string]storage.DownloadInfo, err error) {
  58. rc = make(map[string]storage.DownloadInfo)
  59. backup, err := ps.SignDownload(ctx, rs.Bucket(workspaceOwner), rs.BackupObject(storage.DefaultBackup), &storage.SignedURLOptions{})
  60. if err == storage.ErrNotFound {
  61. // no backup found - that's fine
  62. } else if err != nil {
  63. return nil, err
  64. } else {
  65. rc[storage.DefaultBackup] = *backup
  66. }
  67. if si := initializer.GetSnapshot(); si != nil {
  68. bkt, obj, err := storage.ParseSnapshotName(si.Snapshot)
  69. if err != nil {
  70. return nil, err
  71. }
  72. info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})
  73. if err == storage.ErrNotFound {
  74. return nil, errCannotFindSnapshot
  75. }
  76. if err != nil {
  77. return nil, xerrors.Errorf("cannot find snapshot: %w", err)
  78. }
  79. rc[si.Snapshot] = *info
  80. }
  81. if si := initializer.GetPrebuild(); si != nil && si.Prebuild != nil && si.Prebuild.Snapshot != "" {
  82. bkt, obj, err := storage.ParseSnapshotName(si.Prebuild.Snapshot)
  83. if err != nil {
  84. return nil, err
  85. }
  86. info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})
  87. if err == storage.ErrNotFound {
  88. // no prebuild found - that's fine
  89. } else if err != nil {
  90. return nil, xerrors.Errorf("cannot find prebuild: %w", err)
  91. } else {
  92. rc[si.Prebuild.Snapshot] = *info
  93. }
  94. }
  95. return rc, nil
  96. }
  97. // RunInitializer runs a content initializer in a user, PID and mount namespace to isolate it from ws-daemon
  98. func RunInitializer(ctx context.Context, destination string, initializer *csapi.WorkspaceInitializer, remoteContent map[string]storage.DownloadInfo, opts RunInitializerOpts) (err error) {
  99. //nolint:ineffassign,staticcheck
  100. span, ctx := opentracing.StartSpanFromContext(ctx, "RunInitializer")
  101. defer tracing.FinishSpan(span, &err)
  102. // it's possible the destination folder doesn't exist yet, because the kubelet hasn't created it yet.
  103. // If we fail to create the folder, it either already exists, or we'll fail when we try and mount it.
  104. err = os.MkdirAll(destination, 0755)
  105. if err != nil && !os.IsExist(err) {
  106. return xerrors.Errorf("cannot mkdir destination: %w", err)
  107. }
  108. init, err := proto.Marshal(initializer)
  109. if err != nil {
  110. return err
  111. }
  112. if opts.GID == 0 {
  113. opts.GID = wsinit.GitpodGID
  114. }
  115. if opts.UID == 0 {
  116. opts.UID = wsinit.GitpodUID
  117. }
  118. tmpdir, err := os.MkdirTemp("", "content-init")
  119. if err != nil {
  120. return err
  121. }
  122. defer os.RemoveAll(tmpdir)
  123. err = os.MkdirAll(filepath.Join(tmpdir, "rootfs"), 0755)
  124. if err != nil {
  125. return err
  126. }
  127. msg := msgInitContent{
  128. Destination: "/dst",
  129. Initializer: init,
  130. RemoteContent: remoteContent,
  131. TraceInfo: tracing.GetTraceID(span),
  132. IDMappings: opts.IdMappings,
  133. GID: int(opts.GID),
  134. UID: int(opts.UID),
  135. OWI: opts.OWI.Fields(),
  136. }
  137. fc, err := json.MarshalIndent(msg, "", " ")
  138. if err != nil {
  139. return err
  140. }
  141. err = os.WriteFile(filepath.Join(tmpdir, "rootfs", "content.json"), fc, 0644)
  142. if err != nil {
  143. return err
  144. }
  145. spec := specconv.Example()
  146. // we assemble the root filesystem from the ws-daemon container
  147. for _, d := range []string{"app", "bin", "dev", "etc", "lib", "opt", "sbin", "sys", "usr", "var"} {
  148. spec.Mounts = append(spec.Mounts, specs.Mount{
  149. Destination: "/" + d,
  150. Source: "/" + d,
  151. Type: "bind",
  152. Options: []string{"rbind", "rprivate"},
  153. })
  154. }
  155. spec.Mounts = append(spec.Mounts, specs.Mount{
  156. Destination: "/dst",
  157. Source: destination,
  158. Type: "bind",
  159. Options: []string{"bind", "rprivate"},
  160. })
  161. spec.Hostname = "content-init"
  162. spec.Process.Terminal = false
  163. spec.Process.NoNewPrivileges = true
  164. spec.Process.User.UID = opts.UID
  165. spec.Process.User.GID = opts.GID
  166. spec.Process.Args = []string{"/app/content-initializer"}
  167. for _, e := range os.Environ() {
  168. if strings.HasPrefix(e, "JAEGER_") {
  169. spec.Process.Env = append(spec.Process.Env, e)
  170. }
  171. }
  172. // TODO(cw): make the initializer work without chown
  173. spec.Process.Capabilities.Ambient = append(spec.Process.Capabilities.Ambient, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
  174. spec.Process.Capabilities.Bounding = append(spec.Process.Capabilities.Bounding, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
  175. spec.Process.Capabilities.Effective = append(spec.Process.Capabilities.Effective, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
  176. spec.Process.Capabilities.Inheritable = append(spec.Process.Capabilities.Inheritable, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
  177. spec.Process.Capabilities.Permitted = append(spec.Process.Capabilities.Permitted, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
  178. // TODO(cw): setup proper networking in a netns, rather than relying on ws-daemons network
  179. n := 0
  180. for _, x := range spec.Linux.Namespaces {
  181. if x.Type == specs.NetworkNamespace {
  182. continue
  183. }
  184. spec.Linux.Namespaces[n] = x
  185. n++
  186. }
  187. spec.Linux.Namespaces = spec.Linux.Namespaces[:n]
  188. fc, err = json.MarshalIndent(spec, "", " ")
  189. if err != nil {
  190. return err
  191. }
  192. err = os.WriteFile(filepath.Join(tmpdir, "config.json"), fc, 0644)
  193. if err != nil {
  194. return err
  195. }
  196. args := []string{"--root", "state"}
  197. if log.Log.Logger.IsLevelEnabled(logrus.DebugLevel) {
  198. args = append(args, "--debug")
  199. }
  200. var name string
  201. if opts.OWI.InstanceID == "" {
  202. id, err := uuid.NewRandom()
  203. if err != nil {
  204. return err
  205. }
  206. name = "init-rnd-" + id.String()
  207. } else {
  208. name = "init-ws-" + opts.OWI.InstanceID
  209. }
  210. args = append(args, "--log-format", "json", "run")
  211. args = append(args, "--preserve-fds", "1")
  212. args = append(args, name)
  213. errIn, errOut, err := os.Pipe()
  214. if err != nil {
  215. return err
  216. }
  217. errch := make(chan []byte, 1)
  218. go func() {
  219. errmsg, _ := ioutil.ReadAll(errIn)
  220. errch <- errmsg
  221. }()
  222. var cmdOut bytes.Buffer
  223. cmd := exec.Command("runc", args...)
  224. cmd.Dir = tmpdir
  225. cmd.Stdout = &cmdOut
  226. cmd.Stderr = os.Stderr
  227. cmd.Stdin = os.Stdin
  228. cmd.ExtraFiles = []*os.File{errOut}
  229. err = cmd.Run()
  230. log.FromBuffer(&cmdOut, log.WithFields(opts.OWI.Fields()))
  231. errOut.Close()
  232. var errmsg []byte
  233. select {
  234. case errmsg = <-errch:
  235. case <-time.After(1 * time.Second):
  236. errmsg = []byte("failed to read content initializer response")
  237. }
  238. if err != nil {
  239. if exiterr, ok := err.(*exec.ExitError); ok {
  240. // The program has exited with an exit code != 0. If it's FAIL_CONTENT_INITIALIZER_EXIT_CODE, it was deliberate.
  241. if status, ok := exiterr.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == FAIL_CONTENT_INITIALIZER_EXIT_CODE {
  242. log.WithError(err).WithField("exitCode", status.ExitStatus()).WithField("args", args).Error("content init failed")
  243. return xerrors.Errorf(string(errmsg))
  244. }
  245. }
  246. return err
  247. }
  248. return nil
  249. }
  250. // RunInitializerChild is the function that's expected to run when we call `/proc/self/exe content-initializer`
  251. func RunInitializerChild() (err error) {
  252. fc, err := os.ReadFile("/content.json")
  253. if err != nil {
  254. return err
  255. }
  256. var initmsg msgInitContent
  257. err = json.Unmarshal(fc, &initmsg)
  258. if err != nil {
  259. return err
  260. }
  261. log.Log = logrus.WithFields(initmsg.OWI)
  262. defer func() {
  263. if err != nil {
  264. log.WithError(err).WithFields(initmsg.OWI).Error("content init failed")
  265. }
  266. }()
  267. span := opentracing.StartSpan("RunInitializerChild", opentracing.ChildOf(tracing.FromTraceID(initmsg.TraceInfo)))
  268. defer tracing.FinishSpan(span, &err)
  269. ctx := opentracing.ContextWithSpan(context.Background(), span)
  270. var req csapi.WorkspaceInitializer
  271. err = proto.Unmarshal(initmsg.Initializer, &req)
  272. if err != nil {
  273. return err
  274. }
  275. rs := &remoteContentStorage{RemoteContent: initmsg.RemoteContent}
  276. initializer, err := wsinit.NewFromRequest(ctx, "/dst", rs, &req, wsinit.NewFromRequestOpts{ForceGitpodUserForGit: false})
  277. if err != nil {
  278. return err
  279. }
  280. initSource, err := wsinit.InitializeWorkspace(ctx, "/dst", rs,
  281. wsinit.WithInitializer(initializer),
  282. wsinit.WithMappings(initmsg.IDMappings),
  283. wsinit.WithChown(initmsg.UID, initmsg.GID),
  284. wsinit.WithCleanSlate,
  285. )
  286. if err != nil {
  287. return err
  288. }
  289. // some workspace content may have a `/dst/.gitpod` file or directory. That would break
  290. // the workspace ready file placement (see https://github.com/gitpod-io/gitpod/issues/7694).
  291. err = wsinit.EnsureCleanDotGitpodDirectory(ctx, "/dst")
  292. if err != nil {
  293. return err
  294. }
  295. // Place the ready file to make Theia "open its gates"
  296. err = wsinit.PlaceWorkspaceReadyFile(ctx, "/dst", initSource, initmsg.UID, initmsg.GID)
  297. if err != nil {
  298. return err
  299. }
  300. return nil
  301. }
  302. var _ storage.DirectAccess = &remoteContentStorage{}
  303. type remoteContentStorage struct {
  304. RemoteContent map[string]storage.DownloadInfo
  305. }
  306. // Init does nothing
  307. func (rs *remoteContentStorage) Init(ctx context.Context, owner, workspace, instance string) error {
  308. return nil
  309. }
  310. // EnsureExists does nothing
  311. func (rs *remoteContentStorage) EnsureExists(ctx context.Context) error {
  312. return nil
  313. }
  314. // Download always returns false and does nothing
  315. func (rs *remoteContentStorage) Download(ctx context.Context, destination string, name string, mappings []archive.IDMapping) (exists bool, err error) {
  316. info, exists := rs.RemoteContent[name]
  317. if !exists {
  318. return false, nil
  319. }
  320. resp, err := http.Get(info.URL)
  321. if err != nil {
  322. return true, err
  323. }
  324. defer resp.Body.Close()
  325. err = archive.ExtractTarbal(ctx, resp.Body, destination, archive.WithUIDMapping(mappings), archive.WithGIDMapping(mappings))
  326. if err != nil {
  327. return true, xerrors.Errorf("tar %s: %s", destination, err.Error())
  328. }
  329. return true, nil
  330. }
  331. // DownloadSnapshot always returns false and does nothing
  332. func (rs *remoteContentStorage) DownloadSnapshot(ctx context.Context, destination string, name string, mappings []archive.IDMapping) (bool, error) {
  333. return rs.Download(ctx, destination, name, mappings)
  334. }
  335. // ListObjects returns all objects found with the given prefix. Returns an empty list if the bucket does not exuist (yet).
  336. func (rs *remoteContentStorage) ListObjects(ctx context.Context, prefix string) (objects []string, err error) {
  337. return []string{}, nil
  338. }
  339. // Qualify just returns the name
  340. func (rs *remoteContentStorage) Qualify(name string) string {
  341. return name
  342. }
  343. // Upload does nothing
  344. func (rs *remoteContentStorage) Upload(ctx context.Context, source string, name string, opts ...storage.UploadOption) (string, string, error) {
  345. return "", "", xerrors.Errorf("not implemented")
  346. }
  347. // UploadInstance takes all files from a local location and uploads it to the remote storage
  348. func (rs *remoteContentStorage) UploadInstance(ctx context.Context, source string, name string, options ...storage.UploadOption) (bucket, obj string, err error) {
  349. return "", "", xerrors.Errorf("not implemented")
  350. }
  351. // Bucket returns an empty string
  352. func (rs *remoteContentStorage) Bucket(string) string {
  353. return ""
  354. }
  355. // BackupObject returns a backup's object name that a direct downloader would download
  356. func (rs *remoteContentStorage) BackupObject(name string) string {
  357. return ""
  358. }
  359. // InstanceObject returns a instance's object name that a direct downloader would download
  360. func (rs *remoteContentStorage) InstanceObject(workspaceID string, instanceID string, name string) string {
  361. return ""
  362. }
  363. // SnapshotObject returns a snapshot's object name that a direct downloer would download
  364. func (rs *remoteContentStorage) SnapshotObject(name string) string {
  365. return ""
  366. }
  367. type msgInitContent struct {
  368. Destination string
  369. RemoteContent map[string]storage.DownloadInfo
  370. Initializer []byte
  371. UID, GID int
  372. IDMappings []archive.IDMapping
  373. TraceInfo string
  374. OWI map[string]interface{}
  375. }