Skip to content

[docker] Introduce socket activated docker-up #4018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 21, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions components/docker-up/docker-up/main.go
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions components/docker-up/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
1 change: 1 addition & 0 deletions components/supervisor/go.mod
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions components/supervisor/go.sum
Original file line number Diff line number Diff line change
@@ -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=
73 changes: 73 additions & 0 deletions components/supervisor/pkg/activation/activation.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions components/supervisor/pkg/supervisor/git.go
Original file line number Diff line number Diff line change
@@ -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
51 changes: 50 additions & 1 deletion components/supervisor/pkg/supervisor/supervisor.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions components/supervisor/pkg/terminal/service.go
Original file line number Diff line number Diff line change
@@ -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 {
25 changes: 20 additions & 5 deletions components/supervisor/pkg/terminal/terminal.go
Original file line number Diff line number Diff line change
@@ -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 {
14 changes: 0 additions & 14 deletions components/workspacekit/cmd/rings.go
Original file line number Diff line number Diff line change
@@ -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")
1 change: 0 additions & 1 deletion components/workspacekit/go.mod
Original file line number Diff line number Diff line change
@@ -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
4 changes: 0 additions & 4 deletions components/workspacekit/go.sum
Original file line number Diff line number Diff line change
@@ -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=