diff --git a/components/docker-up/docker-up/main.go b/components/docker-up/docker-up/main.go index 60dcd9f1b2d9bb..7b055f8309adcf 100644 --- a/components/docker-up/docker-up/main.go +++ b/components/docker-up/docker-up/main.go @@ -28,6 +28,7 @@ import ( sigproxysignal "github.com/rootless-containers/rootlesskit/pkg/sigproxy/signal" "github.com/sirupsen/logrus" "github.com/spf13/pflag" + "golang.org/x/sys/unix" ) var log *logrus.Entry @@ -45,10 +46,14 @@ var opts struct { //go:embed slirp4netns var binaries embed.FS +const ( + dockerSocketFN = "/var/run/docker.sock" +) + func main() { self, err := os.Executable() if err != nil { - log.WithError(err).Fatal() + logrus.WithError(err).Fatal() } pflag.BoolVarP(&opts.Verbose, "verbose", "v", false, "enables verbose logging") @@ -68,6 +73,11 @@ func main() { cmd = args[0] } + listenFD := os.Getenv("LISTEN_FDS") != "" + if _, err := os.Stat(dockerSocketFN); !listenFD && (err == nil || !os.IsNotExist(err)) { + logger.Fatalf("Docker socket already exists at %s.\nIn a Gitpod workspace Docker will start automatically when used.\nIf all else fails, please remove %s and try again.", dockerSocketFN, dockerSocketFN) + } + switch cmd { case "child": log = logger.WithField("service", "runWithinNetns") @@ -83,8 +93,10 @@ func main() { } func runWithinNetns() (err error) { - // magic file descriptor 3 was passed in from the parent using ExtraFiles - fd := os.NewFile(uintptr(3), "") + listenFDs, _ := strconv.Atoi(os.Getenv("LISTEN_FDS")) + + // magic file descriptor 3+listenFDs was passed in from the parent using ExtraFiles + fd := os.NewFile(uintptr(3+listenFDs), "") defer fd.Close() log.Debug("waiting for parent") @@ -116,6 +128,19 @@ func runWithinNetns() (err error) { ) } + if listenFDs > 0 { + os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) + args = append(args, "-H", "fd://") + + dockerd, err := exec.LookPath("dockerd") + if err != nil { + return err + } + argv := []string{dockerd} + argv = append(argv, args...) + return unix.Exec(dockerd, argv, os.Environ()) + } + cmd := exec.Command("dockerd", args...) log.WithField("args", args).Debug("starting dockerd") cmd.SysProcAttr = &syscall.SysProcAttr{ @@ -136,7 +161,7 @@ func runWithinNetns() (err error) { if opts.UserAccessibleSocket { go func() { for { - err := os.Chmod("/var/run/docker.sock", 0666) + err := os.Chmod(dockerSocketFN, 0666) if os.IsNotExist(err) { time.Sleep(500 * time.Millisecond) @@ -181,13 +206,22 @@ func runOutsideNetns() error { Pdeathsig: syscall.SIGKILL, Unshareflags: syscall.CLONE_NEWNET, } - cmd.ExtraFiles = []*os.File{pipeR} + if fds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")); err == nil { + for i := 0; i < fds; i++ { + fmt.Printf("passing fd %d\n", i) + cmd.ExtraFiles = append(cmd.ExtraFiles, os.NewFile(uintptr(3+i), "")) + } + } else { + log.WithError(err).WithField("LISTEN_FDS", os.Getenv("listen_fds")).Warn("no LISTEN_FDS") + } + cmd.ExtraFiles = append(cmd.ExtraFiles, pipeR) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), "DOCKERUP_SLIRP4NETNS_SOCKET="+slirpAPI.Name(), ) + err = cmd.Start() if err != nil { return err diff --git a/components/docker-up/go.mod b/components/docker-up/go.mod index a5f4b689ecaf80..e20f6d06dab752 100644 --- a/components/docker-up/go.mod +++ b/components/docker-up/go.mod @@ -7,5 +7,6 @@ require ( github.com/rootless-containers/rootlesskit v0.14.1 github.com/sirupsen/logrus v1.8.1 github.com/spf13/pflag v1.0.5 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) diff --git a/components/supervisor/go.mod b/components/supervisor/go.mod index 29528c272bc14d..bc8ae4d27dec18 100644 --- a/components/supervisor/go.mod +++ b/components/supervisor/go.mod @@ -22,6 +22,7 @@ require ( github.com/google/uuid v1.1.4 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0 + github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/prometheus/procfs v0.6.0 github.com/sirupsen/logrus v1.7.0 diff --git a/components/supervisor/go.sum b/components/supervisor/go.sum index e06bc9492c542e..359a7eb37bac1a 100644 --- a/components/supervisor/go.sum +++ b/components/supervisor/go.sum @@ -425,6 +425,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f h1:4+gHs0jJFJ06bfN8PshnM6cHcxGjRUVRLo5jndDiKRQ= +github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f/go.mod h1:tHCZHV8b2A90ObojrEAzY0Lb03gxUxjDHr5IJyAh4ew= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= diff --git a/components/supervisor/pkg/activation/activation.go b/components/supervisor/pkg/activation/activation.go new file mode 100644 index 00000000000000..0e97ea85b9375f --- /dev/null +++ b/components/supervisor/pkg/activation/activation.go @@ -0,0 +1,73 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package activation + +import ( + "context" + "fmt" + "net" + "os" + "sync" + + "github.com/mailru/easygo/netpoll" +) + +// Callback is called when a listener is written to. Receivers are expected to close socketFD. +type Callback func(socketFD *os.File) error + +// Listen polls on the listener and calls callback when someone writes to it +func Listen(ctx context.Context, l net.Listener, activate Callback) error { + poller, err := netpoll.New(nil) + if err != nil { + return err + } + + // Get netpoll descriptor with EventRead|EventEdgeTriggered. + desc, err := netpoll.HandleListener(l, netpoll.EventRead|netpoll.EventEdgeTriggered) + if err != nil { + return err + } + + var ( + runc = make(chan bool, 1) + once sync.Once + ) + poller.Start(desc, func(ev netpoll.Event) { + defer once.Do(func() { + poller.Stop(desc) + + close(runc) + }) + + if ev&netpoll.EventReadHup != 0 { + return + } + + runc <- true + }) + + select { + case run := <-runc: + if !run { + return nil + } + case <-ctx.Done(): + return ctx.Err() + } + + var f *os.File + switch ll := l.(type) { + case *net.UnixListener: + f, err = ll.File() + case *net.TCPListener: + f, err = ll.File() + default: + return fmt.Errorf("unsuported listener") + } + if err != nil { + return err + } + return activate(f) +} diff --git a/components/supervisor/pkg/supervisor/git.go b/components/supervisor/pkg/supervisor/git.go index 4cf89bf48742d5..c1e648f40aa640 100644 --- a/components/supervisor/pkg/supervisor/git.go +++ b/components/supervisor/pkg/supervisor/git.go @@ -66,6 +66,7 @@ func (p *GitTokenProvider) GetToken(ctx context.Context, req *api.GetTokenReques return nil, err } gpCmd := exec.Command(gpPath, "preview", "--external", p.workspaceConfig.GitpodHost+"/access-control") + gpCmd = runAsGitpodUser(gpCmd) err = gpCmd.Start() if err != nil { return nil, err diff --git a/components/supervisor/pkg/supervisor/supervisor.go b/components/supervisor/pkg/supervisor/supervisor.go index 7071e7b99ba52d..c1ee53b52de68d 100644 --- a/components/supervisor/pkg/supervisor/supervisor.go +++ b/components/supervisor/pkg/supervisor/supervisor.go @@ -33,6 +33,7 @@ import ( "github.com/gitpod-io/gitpod/content-service/pkg/initializer" gitpod "github.com/gitpod-io/gitpod/gitpod-protocol" "github.com/gitpod-io/gitpod/supervisor/api" + "github.com/gitpod-io/gitpod/supervisor/pkg/activation" "github.com/gitpod-io/gitpod/supervisor/pkg/dropwriter" "github.com/gitpod-io/gitpod/supervisor/pkg/ports" "github.com/gitpod-io/gitpod/supervisor/pkg/terminal" @@ -153,6 +154,10 @@ func Run(options ...RunOption) { termMuxSrv.DefaultWorkdir = cfg.RepoRoot termMuxSrv.Env = buildIDEEnv(cfg) + termMuxSrv.DefaultCreds = &syscall.Credential{ + Uid: 33333, + Gid: 33333, + } apiServices := []RegisterableService{ &statusService{ @@ -188,6 +193,7 @@ func Run(options ...RunOption) { go startContentInit(ctx, cfg, &wg, cstate) go startAPIEndpoint(ctx, cfg, &wg, apiServices, apiEndpointOpts...) go taskManager.Run(ctx, &wg) + go socketActivationForDocker(ctx, &wg, termMux) if !cfg.isHeadless() { wg.Add(1) @@ -286,6 +292,7 @@ func configureGit(cfg *Config) { for _, s := range settings { cmd := exec.Command("git", append([]string{"config", "--global"}, s...)...) + cmd = runAsGitpodUser(cmd) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() @@ -473,6 +480,10 @@ func prepareIDELaunch(cfg *Config) *exec.Cmd { cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pdeathsig: syscall.SIGKILL, + Credential: &syscall.Credential{ + Uid: 33333, + Gid: 33333, + }, } // Here we must resist the temptation to "neaten up" the IDE output for headless builds. @@ -717,7 +728,7 @@ func terminateChildProcesses() { func terminateProcess(pid int, privileged bool) { var err error if privileged { - cmd := exec.Command("sudo", "kill", "-SIGTERM", fmt.Sprintf("%v", pid)) + cmd := exec.Command("kill", "-SIGTERM", fmt.Sprintf("%v", pid)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() @@ -767,6 +778,32 @@ func processesWithParent(ppid int) (map[int]int, error) { return children, nil } +func socketActivationForDocker(ctx context.Context, wg *sync.WaitGroup, term *terminal.Mux) { + defer wg.Done() + + fn := "/var/run/docker.sock" + l, err := net.Listen("unix", fn) + if err != nil { + log.WithError(err).Error("cannot provide Docker activation socket") + } + _ = os.Chown(fn, 33333, 33333) + err = activation.Listen(ctx, l, func(socketFD *os.File) error { + cmd := exec.Command("docker-up") + cmd.Env = append(os.Environ(), "LISTEN_FDS=1") + cmd.ExtraFiles = []*os.File{socketFD} + _, err := term.Start(cmd, terminal.TermOptions{ + Annotations: map[string]string{ + "supervisor": "true", + }, + LogToStdout: true, + }) + return err + }) + if err != nil { + log.WithError(err).Error("cannot provide Docker activation socket") + } +} + func callDaemonTeardown() { log.Info("asking ws-daemon to tear down this workspace") ctx, cancel := context.WithTimeout(context.Background(), timeBudgetDaemonTeardown) @@ -810,3 +847,15 @@ func ConnectToInWorkspaceDaemonService(ctx context.Context) (daemon.InWorkspaceS } return daemon.NewInWorkspaceServiceClient(conn), conn, nil } + +func runAsGitpodUser(cmd *exec.Cmd) *exec.Cmd { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + if cmd.SysProcAttr.Credential == nil { + cmd.SysProcAttr.Credential = &syscall.Credential{} + } + cmd.SysProcAttr.Credential.Gid = 33333 + cmd.SysProcAttr.Credential.Uid = 33333 + return cmd +} diff --git a/components/supervisor/pkg/terminal/service.go b/components/supervisor/pkg/terminal/service.go index 0000542e2a15f9..86979a0a6eb02f 100644 --- a/components/supervisor/pkg/terminal/service.go +++ b/components/supervisor/pkg/terminal/service.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "syscall" "time" "github.com/creack/pty" @@ -50,6 +51,7 @@ type MuxTerminalService struct { DefaultWorkdir string DefaultShell string Env []string + DefaultCreds *syscall.Credential api.UnimplementedTerminalServiceServer } @@ -80,6 +82,11 @@ func (srv *MuxTerminalService) OpenWithOptions(ctx context.Context, req *api.Ope shell = srv.DefaultShell } cmd := exec.Command(shell, req.ShellArgs...) + if srv.DefaultCreds != nil { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: srv.DefaultCreds, + } + } if req.Workdir == "" { cmd.Dir = srv.DefaultWorkdir } else { diff --git a/components/supervisor/pkg/terminal/terminal.go b/components/supervisor/pkg/terminal/terminal.go index 6dce50b716b4d4..fde8c993a04638 100644 --- a/components/supervisor/pkg/terminal/terminal.go +++ b/components/supervisor/pkg/terminal/terminal.go @@ -17,6 +17,7 @@ import ( "github.com/creack/pty" "github.com/google/uuid" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" "golang.org/x/xerrors" @@ -62,7 +63,7 @@ func (m *Mux) Start(cmd *exec.Cmd, options TermOptions) (alias string, err error } alias = uid.String() - term, err := newTerm(pty, cmd, options) + term, err := newTerm(alias, pty, cmd, options) if err != nil { pty.Close() return "", err @@ -188,7 +189,7 @@ func (term *Term) shutdownProcessImmediately() error { // For now we assume an average of five terminals per workspace, which makes this consume 1MiB of RAM. const terminalBacklogSize = 256 << 10 -func newTerm(pty *os.File, cmd *exec.Cmd, options TermOptions) (*Term, error) { +func newTerm(alias string, pty *os.File, cmd *exec.Cmd, options TermOptions) (*Term, error) { token, err := uuid.NewRandom() if err != nil { return nil, err @@ -207,9 +208,11 @@ func newTerm(pty *os.File, cmd *exec.Cmd, options TermOptions) (*Term, error) { PTY: pty, Command: cmd, Stdout: &multiWriter{ - timeout: timeout, - listener: make(map[*multiWriterListener]struct{}), - recorder: recorder, + timeout: timeout, + listener: make(map[*multiWriterListener]struct{}), + recorder: recorder, + logStdout: options.LogToStdout, + logLabel: alias, }, Annotations: options.Annotations, title: options.Title, @@ -245,6 +248,9 @@ type TermOptions struct { // Title describes the terminal title. Title string + + // LogToStdout forwards the terminal's stdout to supervisor's stdout + LogToStdout bool } // Term is a pseudo-terminal @@ -312,6 +318,9 @@ type multiWriter struct { // ring buffer to record last 256kb of pty output // new listener is initialized with the latest recodring first recorder *RingBuffer + + logStdout bool + logLabel string } var ( @@ -424,6 +433,12 @@ func (mw *multiWriter) Write(p []byte) (n int, err error) { defer mw.mu.Unlock() mw.recorder.Write(p) + if mw.logStdout { + log.WithFields(logrus.Fields{ + "terminalOutput": true, + "label": mw.logLabel, + }).Info(string(p)) + } for lstr := range mw.listener { if lstr.closed { diff --git a/components/workspacekit/cmd/rings.go b/components/workspacekit/cmd/rings.go index 92d0eca87dd8ad..3bef4912d95fff 100644 --- a/components/workspacekit/cmd/rings.go +++ b/components/workspacekit/cmd/rings.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/sys/unix" "google.golang.org/grpc" - "kernel.org/pub/linux/libs/security/libcap/cap" "github.com/gitpod-io/gitpod/common-go/log" "github.com/gitpod-io/gitpod/workspacekit/pkg/lift" @@ -570,19 +569,6 @@ var ring2Cmd = &cobra.Command{ return } - err = cap.SetGroups(33333) - if err != nil { - log.WithError(err).Error("cannot setgid") - failed = true - return - } - err = cap.SetUID(33333) - if err != nil { - log.WithError(err).Error("cannot setuid") - failed = true - return - } - err = unix.Exec(ring2Opts.SupervisorPath, []string{"supervisor", "run", "--inns"}, os.Environ()) if err != nil { log.WithError(err).WithField("cmd", ring2Opts.SupervisorPath).Error("cannot exec") diff --git a/components/workspacekit/go.mod b/components/workspacekit/go.mod index e62ad0ed579324..e9b216cb1229f4 100644 --- a/components/workspacekit/go.mod +++ b/components/workspacekit/go.mod @@ -13,7 +13,6 @@ require ( github.com/spf13/cobra v1.1.1 golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 google.golang.org/grpc v1.36.0 - kernel.org/pub/linux/libs/security/libcap/cap v0.2.46 ) replace github.com/gitpod-io/gitpod/common-go => ../common-go // leeway diff --git a/components/workspacekit/go.sum b/components/workspacekit/go.sum index b6b3dd9efe46ec..aa8e986159a3b1 100644 --- a/components/workspacekit/go.sum +++ b/components/workspacekit/go.sum @@ -461,10 +461,6 @@ k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -kernel.org/pub/linux/libs/security/libcap/cap v0.2.46 h1:2my+JWsYxD0mFKUbqgtEf7r9A0m/fCMUv21RGgknTiU= -kernel.org/pub/linux/libs/security/libcap/cap v0.2.46/go.mod h1:Xni6/5rCuzPoHAac5sCFMuDxz9FuI8GTUyQ4qlw3e0w= -kernel.org/pub/linux/libs/security/libcap/psx v0.2.46 h1:9GvXrCSQAcgQ3zZVxRN8K866o1aAY1DYdXj0vHIHvYA= -kernel.org/pub/linux/libs/security/libcap/psx v0.2.46/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=