github.com/mgoltzsche/ctnr@v0.7.1-alpha/run/runcrunner/runccontainer.go (about) 1 package runcrunner 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "sync" 11 "syscall" 12 "time" 13 14 exterrors "github.com/mgoltzsche/ctnr/pkg/errors" 15 "github.com/mgoltzsche/ctnr/pkg/log" 16 "github.com/mgoltzsche/ctnr/run" 17 "github.com/opencontainers/runtime-spec/specs-go" 18 "github.com/pkg/errors" 19 ) 20 21 type RuncContainer struct { 22 io run.ContainerIO 23 id string 24 bundleDir string 25 rootfs string 26 noNewKeyring bool 27 noPivot bool 28 destroyOnClose bool 29 rootDir string 30 process *RuncProcess 31 created bool 32 debug log.FieldLogger 33 err error 34 } 35 36 func NewRuncContainer(cfg *run.ContainerConfig, rootDir string, debug log.FieldLogger) (c *RuncContainer, err error) { 37 id := cfg.Id 38 if id == "" { 39 if id = cfg.Bundle.ID(); id == "" { 40 panic("no container ID provided and bundle ID is empty") 41 } 42 } 43 44 spec, err := cfg.Bundle.Spec() 45 if err != nil { 46 return nil, errors.Wrapf(err, "new container %q", id) 47 } 48 49 // TODO: handle config option destroyOnClose 50 c = &RuncContainer{ 51 id: id, 52 bundleDir: cfg.Bundle.Dir(), 53 io: cfg.Io, 54 rootfs: filepath.Join(cfg.Bundle.Dir(), spec.Root.Path), 55 noPivot: cfg.NoPivotRoot, 56 noNewKeyring: cfg.NoNewKeyring, 57 destroyOnClose: cfg.DestroyOnClose, 58 rootDir: rootDir, 59 debug: debug.WithField("id", id), 60 } 61 62 // Create process 63 c.process = NewRuncProcess(c.runcCreateArgs("run", "--bundle", cfg.Bundle.Dir(), c.ID()), spec.Process.Terminal, cfg.Io, c.debug) 64 return 65 } 66 67 func (c *RuncContainer) Close() (err error) { 68 c.Stop() 69 err = c.Wait() 70 if c.destroyOnClose { 71 err = exterrors.Append(err, c.destroy()) 72 } 73 return 74 } 75 76 func (c *RuncContainer) ID() string { 77 return c.id 78 } 79 80 func (c *RuncContainer) Rootfs() string { 81 return c.rootfs 82 } 83 84 func (c *RuncContainer) Start() (err error) { 85 c.debug.Println("Starting container") 86 return c.process.Start() 87 } 88 89 func (c *RuncContainer) runcCreateArgs(cmd string, a ...string) []string { 90 args := c.runcArgs(cmd) 91 if c.noPivot { 92 args = append(args, "--no-pivot") 93 } 94 if c.noNewKeyring { 95 args = append(args, "--no-new-keyring") 96 } 97 return append(args, a...) 98 } 99 100 func (c *RuncContainer) runcArgs(a ...string) []string { 101 return append(append(make([]string, 0, len(a)+3), "runc", "--root", c.rootDir), a...) 102 } 103 104 func (c *RuncContainer) Stop() { 105 c.process.Stop() 106 } 107 108 func (c *RuncContainer) Wait() error { 109 return c.process.Wait() 110 } 111 112 func (c *RuncContainer) Destroy() (err error) { 113 err = c.Close() 114 return exterrors.Append(err, c.destroy()) 115 } 116 117 func (c *RuncContainer) destroy() (err error) { 118 c.debug.Println("Destroying container") 119 120 // Reset bundle expiry time 121 bundleDir := filepath.Join(c.Rootfs(), "..") 122 now := time.Now() 123 e := errors.Wrap(os.Chtimes(bundleDir, now, now), "reset bundle expiry time") 124 err = exterrors.Append(err, e) 125 126 // Destroy container 127 err = exterrors.Append(err, c.run("", "runc", "--root", c.rootDir, "delete", c.ID())) // TODO: Add --force option 128 return 129 } 130 131 func (c *RuncContainer) run(args ...string) (err error) { 132 var stderr bytes.Buffer 133 p := exec.Command(args[0], args[1:]...) 134 p.Stderr = &stderr 135 err = p.Run() 136 return errors.Wrapf(err, "exec %+v (stderr: %s)", args, stderr.String()) 137 } 138 139 func (c *RuncContainer) Exec(process *specs.Process, io run.ContainerIO) (proc run.Process, err error) { 140 // Create container if not exists 141 exists, err := c.exists() 142 if err != nil { 143 return nil, errors.WithMessage(err, "exec") 144 } 145 if !exists { 146 if err = c.create(); err != nil { 147 return nil, err 148 } 149 } 150 151 // Start process 152 args := c.runcArgs("exec", "--cwd", process.Cwd) 153 if process.Terminal { 154 args = append(args, "-t") 155 } 156 args = append(args, "-u", fmt.Sprintf("%d:%d", process.User.UID, process.User.GID)) 157 for _, envLine := range process.Env { 158 args = append(args, "-e", envLine) 159 } 160 if process.SelinuxLabel != "" { 161 args = append(args, "--process-label", process.SelinuxLabel) 162 } 163 if process.ApparmorProfile != "" { 164 args = append(args, "--apparmor", process.ApparmorProfile) 165 } 166 if process.NoNewPrivileges { 167 args = append(args, "--no-new-privs") 168 } 169 args = append(args, c.ID()) 170 args = append(args, process.Args...) 171 p := NewRuncProcess(args, process.Terminal, io, c.debug) 172 return p, p.Start() 173 } 174 175 func (c *RuncContainer) create() (err error) { 176 c.debug.Println("Creating container") 177 args := c.runcCreateArgs("create", "--bundle", c.bundleDir, c.ID()) 178 p := exec.Command(args[0], args[1:]...) 179 var wg sync.WaitGroup 180 stderr, err := p.StderrPipe() 181 if err != nil { 182 return 183 } 184 stdout, err := p.StdoutPipe() 185 if err != nil { 186 return 187 } 188 wg.Add(2) 189 go copyIO(stderr, os.Stderr, &wg) 190 go copyIO(stdout, os.Stdout, &wg) 191 if err = p.Run(); err == nil { 192 c.created = true 193 } 194 wg.Wait() 195 return 196 } 197 198 func copyIO(r io.ReadCloser, w io.Writer, wg *sync.WaitGroup) { 199 defer wg.Done() 200 io.Copy(w, r) 201 r.Close() 202 } 203 204 func (c *RuncContainer) exists() (r bool, err error) { 205 if c.created { 206 return true, nil 207 } 208 err = c.run(c.runcArgs("state", c.ID())...) 209 if exiterr, ok := errors.Cause(err).(*exec.ExitError); ok { 210 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 211 switch status.ExitStatus() { 212 case 0: 213 return true, nil 214 case 1: 215 return false, nil 216 default: 217 return false, errors.New(exiterr.Error() + ", stderr: " + string(exiterr.Stderr)) 218 } 219 } 220 } 221 return 222 }