mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Merge gvisor-containerd-shim
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runsc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bytesBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(nil)
|
||||
},
|
||||
}
|
||||
|
||||
func getBuf() *bytes.Buffer {
|
||||
return bytesBufferPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
func putBuf(b *bytes.Buffer) {
|
||||
b.Reset()
|
||||
bytesBufferPool.Put(b)
|
||||
}
|
||||
|
||||
// FormatLogPath parses runsc config, and fill in %ID% in the log path.
|
||||
func FormatLogPath(id string, config map[string]string) {
|
||||
if path, ok := config["debug-log"]; ok {
|
||||
config["debug-log"] = strings.Replace(path, "%ID%", id, -1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/runtime/proc"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type deletedState struct{}
|
||||
|
||||
func (*deletedState) Resize(ws console.WinSize) error {
|
||||
return errors.Errorf("cannot resize a deleted process")
|
||||
}
|
||||
|
||||
func (*deletedState) Start(ctx context.Context) error {
|
||||
return errors.Errorf("cannot start a deleted process")
|
||||
}
|
||||
|
||||
func (*deletedState) Delete(ctx context.Context) error {
|
||||
return errors.Wrap(errdefs.ErrNotFound, "cannot delete a deleted process")
|
||||
}
|
||||
|
||||
func (*deletedState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return errors.Wrap(errdefs.ErrNotFound, "cannot kill a deleted process")
|
||||
}
|
||||
|
||||
func (*deletedState) SetExited(status int) {}
|
||||
|
||||
func (*deletedState) Exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
return nil, errors.Errorf("cannot exec in a deleted state")
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/runtime/proc"
|
||||
"github.com/containerd/fifo"
|
||||
runc "github.com/containerd/go-runc"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
runsc "github.com/google/gvisor-containerd-shim/pkg/go-runsc"
|
||||
)
|
||||
|
||||
type execProcess struct {
|
||||
wg sync.WaitGroup
|
||||
|
||||
execState execState
|
||||
|
||||
mu sync.Mutex
|
||||
id string
|
||||
console console.Console
|
||||
io runc.IO
|
||||
status int
|
||||
exited time.Time
|
||||
pid int
|
||||
internalPid int
|
||||
closers []io.Closer
|
||||
stdin io.Closer
|
||||
stdio proc.Stdio
|
||||
path string
|
||||
spec specs.Process
|
||||
|
||||
parent *Init
|
||||
waitBlock chan struct{}
|
||||
}
|
||||
|
||||
func (e *execProcess) Wait() {
|
||||
<-e.waitBlock
|
||||
}
|
||||
|
||||
func (e *execProcess) ID() string {
|
||||
return e.id
|
||||
}
|
||||
|
||||
func (e *execProcess) Pid() int {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.pid
|
||||
}
|
||||
|
||||
func (e *execProcess) ExitStatus() int {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.status
|
||||
}
|
||||
|
||||
func (e *execProcess) ExitedAt() time.Time {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.exited
|
||||
}
|
||||
|
||||
func (e *execProcess) SetExited(status int) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.execState.SetExited(status)
|
||||
}
|
||||
|
||||
func (e *execProcess) setExited(status int) {
|
||||
e.status = status
|
||||
e.exited = time.Now()
|
||||
e.parent.Platform.ShutdownConsole(context.Background(), e.console)
|
||||
close(e.waitBlock)
|
||||
}
|
||||
|
||||
func (e *execProcess) Delete(ctx context.Context) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.execState.Delete(ctx)
|
||||
}
|
||||
|
||||
func (e *execProcess) delete(ctx context.Context) error {
|
||||
e.wg.Wait()
|
||||
if e.io != nil {
|
||||
for _, c := range e.closers {
|
||||
c.Close()
|
||||
}
|
||||
e.io.Close()
|
||||
}
|
||||
pidfile := filepath.Join(e.path, fmt.Sprintf("%s.pid", e.id))
|
||||
// silently ignore error
|
||||
os.Remove(pidfile)
|
||||
internalPidfile := filepath.Join(e.path, fmt.Sprintf("%s-internal.pid", e.id))
|
||||
// silently ignore error
|
||||
os.Remove(internalPidfile)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *execProcess) Resize(ws console.WinSize) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.execState.Resize(ws)
|
||||
}
|
||||
|
||||
func (e *execProcess) resize(ws console.WinSize) error {
|
||||
if e.console == nil {
|
||||
return nil
|
||||
}
|
||||
return e.console.Resize(ws)
|
||||
}
|
||||
|
||||
func (e *execProcess) Kill(ctx context.Context, sig uint32, _ bool) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.execState.Kill(ctx, sig, false)
|
||||
}
|
||||
|
||||
func (e *execProcess) kill(ctx context.Context, sig uint32, _ bool) error {
|
||||
internalPid := e.internalPid
|
||||
if internalPid != 0 {
|
||||
if err := e.parent.runtime.Kill(ctx, e.parent.id, int(sig), &runsc.KillOpts{
|
||||
Pid: internalPid,
|
||||
}); err != nil {
|
||||
// If this returns error, consider the process has already stopped.
|
||||
// TODO: Fix after signal handling is fixed.
|
||||
return errors.Wrapf(errdefs.ErrNotFound, err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *execProcess) Stdin() io.Closer {
|
||||
return e.stdin
|
||||
}
|
||||
|
||||
func (e *execProcess) Stdio() proc.Stdio {
|
||||
return e.stdio
|
||||
}
|
||||
|
||||
func (e *execProcess) Start(ctx context.Context) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
return e.execState.Start(ctx)
|
||||
}
|
||||
|
||||
func (e *execProcess) start(ctx context.Context) (err error) {
|
||||
var (
|
||||
socket *runc.Socket
|
||||
pidfile = filepath.Join(e.path, fmt.Sprintf("%s.pid", e.id))
|
||||
internalPidfile = filepath.Join(e.path, fmt.Sprintf("%s-internal.pid", e.id))
|
||||
)
|
||||
if e.stdio.Terminal {
|
||||
if socket, err = runc.NewTempConsoleSocket(); err != nil {
|
||||
return errors.Wrap(err, "failed to create runc console socket")
|
||||
}
|
||||
defer socket.Close()
|
||||
} else if e.stdio.IsNull() {
|
||||
if e.io, err = runc.NewNullIO(); err != nil {
|
||||
return errors.Wrap(err, "creating new NULL IO")
|
||||
}
|
||||
} else {
|
||||
if e.io, err = runc.NewPipeIO(e.parent.IoUID, e.parent.IoGID, withConditionalIO(e.stdio)); err != nil {
|
||||
return errors.Wrap(err, "failed to create runc io pipes")
|
||||
}
|
||||
}
|
||||
opts := &runsc.ExecOpts{
|
||||
PidFile: pidfile,
|
||||
InternalPidFile: internalPidfile,
|
||||
IO: e.io,
|
||||
Detach: true,
|
||||
}
|
||||
if socket != nil {
|
||||
opts.ConsoleSocket = socket
|
||||
}
|
||||
eventCh := e.parent.Monitor.Subscribe()
|
||||
defer func() {
|
||||
// Unsubscribe if an error is returned.
|
||||
if err != nil {
|
||||
e.parent.Monitor.Unsubscribe(eventCh)
|
||||
}
|
||||
}()
|
||||
if err := e.parent.runtime.Exec(ctx, e.parent.id, e.spec, opts); err != nil {
|
||||
close(e.waitBlock)
|
||||
return e.parent.runtimeError(err, "OCI runtime exec failed")
|
||||
}
|
||||
if e.stdio.Stdin != "" {
|
||||
sc, err := fifo.OpenFifo(context.Background(), e.stdio.Stdin, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open stdin fifo %s", e.stdio.Stdin)
|
||||
}
|
||||
e.closers = append(e.closers, sc)
|
||||
e.stdin = sc
|
||||
}
|
||||
var copyWaitGroup sync.WaitGroup
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if socket != nil {
|
||||
console, err := socket.ReceiveMaster()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve console master")
|
||||
}
|
||||
if e.console, err = e.parent.Platform.CopyConsole(ctx, console, e.stdio.Stdin, e.stdio.Stdout, e.stdio.Stderr, &e.wg, ©WaitGroup); err != nil {
|
||||
return errors.Wrap(err, "failed to start console copy")
|
||||
}
|
||||
} else if !e.stdio.IsNull() {
|
||||
if err := copyPipes(ctx, e.io, e.stdio.Stdin, e.stdio.Stdout, e.stdio.Stderr, &e.wg, ©WaitGroup); err != nil {
|
||||
return errors.Wrap(err, "failed to start io pipe copy")
|
||||
}
|
||||
}
|
||||
copyWaitGroup.Wait()
|
||||
pid, err := runc.ReadPidFile(opts.PidFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve OCI runtime exec pid")
|
||||
}
|
||||
e.pid = pid
|
||||
internalPid, err := runc.ReadPidFile(opts.InternalPidFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve OCI runtime exec internal pid")
|
||||
}
|
||||
e.internalPid = internalPid
|
||||
go func() {
|
||||
defer e.parent.Monitor.Unsubscribe(eventCh)
|
||||
for event := range eventCh {
|
||||
if event.Pid == e.pid {
|
||||
ExitCh <- Exit{
|
||||
Timestamp: event.Timestamp,
|
||||
ID: e.id,
|
||||
Status: event.Status,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *execProcess) Status(ctx context.Context) (string, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
// if we don't have a pid then the exec process has just been created
|
||||
if e.pid == 0 {
|
||||
return "created", nil
|
||||
}
|
||||
// if we have a pid and it can be signaled, the process is running
|
||||
// TODO(random-liu): Use `runsc kill --pid`.
|
||||
if err := unix.Kill(e.pid, 0); err == nil {
|
||||
return "running", nil
|
||||
}
|
||||
// else if we have a pid but it can nolonger be signaled, it has stopped
|
||||
return "stopped", nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type execState interface {
|
||||
Resize(console.WinSize) error
|
||||
Start(context.Context) error
|
||||
Delete(context.Context) error
|
||||
Kill(context.Context, uint32, bool) error
|
||||
SetExited(int)
|
||||
}
|
||||
|
||||
type execCreatedState struct {
|
||||
p *execProcess
|
||||
}
|
||||
|
||||
func (s *execCreatedState) transition(name string) error {
|
||||
switch name {
|
||||
case "running":
|
||||
s.p.execState = &execRunningState{p: s.p}
|
||||
case "stopped":
|
||||
s.p.execState = &execStoppedState{p: s.p}
|
||||
case "deleted":
|
||||
s.p.execState = &deletedState{}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *execCreatedState) Resize(ws console.WinSize) error {
|
||||
return s.p.resize(ws)
|
||||
}
|
||||
|
||||
func (s *execCreatedState) Start(ctx context.Context) error {
|
||||
if err := s.p.start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.transition("running")
|
||||
}
|
||||
|
||||
func (s *execCreatedState) Delete(ctx context.Context) error {
|
||||
if err := s.p.delete(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.transition("deleted")
|
||||
}
|
||||
|
||||
func (s *execCreatedState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return s.p.kill(ctx, sig, all)
|
||||
}
|
||||
|
||||
func (s *execCreatedState) SetExited(status int) {
|
||||
s.p.setExited(status)
|
||||
|
||||
if err := s.transition("stopped"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type execRunningState struct {
|
||||
p *execProcess
|
||||
}
|
||||
|
||||
func (s *execRunningState) transition(name string) error {
|
||||
switch name {
|
||||
case "stopped":
|
||||
s.p.execState = &execStoppedState{p: s.p}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *execRunningState) Resize(ws console.WinSize) error {
|
||||
return s.p.resize(ws)
|
||||
}
|
||||
|
||||
func (s *execRunningState) Start(ctx context.Context) error {
|
||||
return errors.Errorf("cannot start a running process")
|
||||
}
|
||||
|
||||
func (s *execRunningState) Delete(ctx context.Context) error {
|
||||
return errors.Errorf("cannot delete a running process")
|
||||
}
|
||||
|
||||
func (s *execRunningState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return s.p.kill(ctx, sig, all)
|
||||
}
|
||||
|
||||
func (s *execRunningState) SetExited(status int) {
|
||||
s.p.setExited(status)
|
||||
|
||||
if err := s.transition("stopped"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type execStoppedState struct {
|
||||
p *execProcess
|
||||
}
|
||||
|
||||
func (s *execStoppedState) transition(name string) error {
|
||||
switch name {
|
||||
case "deleted":
|
||||
s.p.execState = &deletedState{}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *execStoppedState) Resize(ws console.WinSize) error {
|
||||
return errors.Errorf("cannot resize a stopped container")
|
||||
}
|
||||
|
||||
func (s *execStoppedState) Start(ctx context.Context) error {
|
||||
return errors.Errorf("cannot start a stopped process")
|
||||
}
|
||||
|
||||
func (s *execStoppedState) Delete(ctx context.Context) error {
|
||||
if err := s.p.delete(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.transition("deleted")
|
||||
}
|
||||
|
||||
func (s *execStoppedState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return s.p.kill(ctx, sig, all)
|
||||
}
|
||||
|
||||
func (s *execStoppedState) SetExited(status int) {
|
||||
// no op
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/mount"
|
||||
"github.com/containerd/containerd/runtime/proc"
|
||||
"github.com/containerd/fifo"
|
||||
runc "github.com/containerd/go-runc"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
runsc "github.com/google/gvisor-containerd-shim/pkg/go-runsc"
|
||||
)
|
||||
|
||||
// InitPidFile name of the file that contains the init pid
|
||||
const InitPidFile = "init.pid"
|
||||
|
||||
// Init represents an initial process for a container
|
||||
type Init struct {
|
||||
wg sync.WaitGroup
|
||||
initState initState
|
||||
|
||||
// mu is used to ensure that `Start()` and `Exited()` calls return in
|
||||
// the right order when invoked in separate go routines.
|
||||
// This is the case within the shim implementation as it makes use of
|
||||
// the reaper interface.
|
||||
mu sync.Mutex
|
||||
|
||||
waitBlock chan struct{}
|
||||
|
||||
WorkDir string
|
||||
|
||||
id string
|
||||
Bundle string
|
||||
console console.Console
|
||||
Platform proc.Platform
|
||||
io runc.IO
|
||||
runtime *runsc.Runsc
|
||||
status int
|
||||
exited time.Time
|
||||
pid int
|
||||
closers []io.Closer
|
||||
stdin io.Closer
|
||||
stdio proc.Stdio
|
||||
Rootfs string
|
||||
IoUID int
|
||||
IoGID int
|
||||
Sandbox bool
|
||||
UserLog string
|
||||
Monitor ProcessMonitor
|
||||
}
|
||||
|
||||
// NewRunsc returns a new runsc instance for a process
|
||||
func NewRunsc(root, path, namespace, runtime string, config map[string]string) *runsc.Runsc {
|
||||
if root == "" {
|
||||
root = RunscRoot
|
||||
}
|
||||
return &runsc.Runsc{
|
||||
Command: runtime,
|
||||
PdeathSignal: syscall.SIGKILL,
|
||||
Log: filepath.Join(path, "log.json"),
|
||||
LogFormat: runc.JSON,
|
||||
Root: filepath.Join(root, namespace),
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new init process
|
||||
func New(id string, runtime *runsc.Runsc, stdio proc.Stdio) *Init {
|
||||
p := &Init{
|
||||
id: id,
|
||||
runtime: runtime,
|
||||
stdio: stdio,
|
||||
status: 0,
|
||||
waitBlock: make(chan struct{}),
|
||||
}
|
||||
p.initState = &createdState{p: p}
|
||||
return p
|
||||
}
|
||||
|
||||
// Create the process with the provided config
|
||||
func (p *Init) Create(ctx context.Context, r *CreateConfig) (err error) {
|
||||
var socket *runc.Socket
|
||||
if r.Terminal {
|
||||
if socket, err = runc.NewTempConsoleSocket(); err != nil {
|
||||
return errors.Wrap(err, "failed to create OCI runtime console socket")
|
||||
}
|
||||
defer socket.Close()
|
||||
} else if hasNoIO(r) {
|
||||
if p.io, err = runc.NewNullIO(); err != nil {
|
||||
return errors.Wrap(err, "creating new NULL IO")
|
||||
}
|
||||
} else {
|
||||
if p.io, err = runc.NewPipeIO(p.IoUID, p.IoGID, withConditionalIO(p.stdio)); err != nil {
|
||||
return errors.Wrap(err, "failed to create OCI runtime io pipes")
|
||||
}
|
||||
}
|
||||
pidFile := filepath.Join(p.Bundle, InitPidFile)
|
||||
opts := &runsc.CreateOpts{
|
||||
PidFile: pidFile,
|
||||
}
|
||||
if socket != nil {
|
||||
opts.ConsoleSocket = socket
|
||||
}
|
||||
if p.Sandbox {
|
||||
opts.IO = p.io
|
||||
// UserLog is only useful for sandbox.
|
||||
opts.UserLog = p.UserLog
|
||||
}
|
||||
if err := p.runtime.Create(ctx, r.ID, r.Bundle, opts); err != nil {
|
||||
return p.runtimeError(err, "OCI runtime create failed")
|
||||
}
|
||||
if r.Stdin != "" {
|
||||
sc, err := fifo.OpenFifo(context.Background(), r.Stdin, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open stdin fifo %s", r.Stdin)
|
||||
}
|
||||
p.stdin = sc
|
||||
p.closers = append(p.closers, sc)
|
||||
}
|
||||
var copyWaitGroup sync.WaitGroup
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if socket != nil {
|
||||
console, err := socket.ReceiveMaster()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve console master")
|
||||
}
|
||||
console, err = p.Platform.CopyConsole(ctx, console, r.Stdin, r.Stdout, r.Stderr, &p.wg, ©WaitGroup)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start console copy")
|
||||
}
|
||||
p.console = console
|
||||
} else if !hasNoIO(r) {
|
||||
if err := copyPipes(ctx, p.io, r.Stdin, r.Stdout, r.Stderr, &p.wg, ©WaitGroup); err != nil {
|
||||
return errors.Wrap(err, "failed to start io pipe copy")
|
||||
}
|
||||
}
|
||||
|
||||
copyWaitGroup.Wait()
|
||||
pid, err := runc.ReadPidFile(pidFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve OCI runtime container pid")
|
||||
}
|
||||
p.pid = pid
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait for the process to exit
|
||||
func (p *Init) Wait() {
|
||||
<-p.waitBlock
|
||||
}
|
||||
|
||||
// ID of the process
|
||||
func (p *Init) ID() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
// Pid of the process
|
||||
func (p *Init) Pid() int {
|
||||
return p.pid
|
||||
}
|
||||
|
||||
// ExitStatus of the process
|
||||
func (p *Init) ExitStatus() int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.status
|
||||
}
|
||||
|
||||
// ExitedAt at time when the process exited
|
||||
func (p *Init) ExitedAt() time.Time {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.exited
|
||||
}
|
||||
|
||||
// Status of the process
|
||||
func (p *Init) Status(ctx context.Context) (string, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
c, err := p.runtime.State(ctx, p.id)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
return "stopped", nil
|
||||
}
|
||||
return "", p.runtimeError(err, "OCI runtime state failed")
|
||||
}
|
||||
return p.convertStatus(c.Status), nil
|
||||
}
|
||||
|
||||
// Start the init process
|
||||
func (p *Init) Start(ctx context.Context) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.initState.Start(ctx)
|
||||
}
|
||||
|
||||
func (p *Init) start(ctx context.Context) error {
|
||||
var cio runc.IO
|
||||
if !p.Sandbox {
|
||||
cio = p.io
|
||||
}
|
||||
if err := p.runtime.Start(ctx, p.id, cio); err != nil {
|
||||
return p.runtimeError(err, "OCI runtime start failed")
|
||||
}
|
||||
go func() {
|
||||
status, err := p.runtime.Wait(context.Background(), p.id)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Errorf("Failed to wait for container %q", p.id)
|
||||
// TODO(random-liu): Handle runsc kill error.
|
||||
if err := p.killAll(ctx); err != nil {
|
||||
log.G(ctx).WithError(err).Errorf("Failed to kill container %q", p.id)
|
||||
}
|
||||
status = internalErrorCode
|
||||
}
|
||||
ExitCh <- Exit{
|
||||
Timestamp: time.Now(),
|
||||
ID: p.id,
|
||||
Status: status,
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExited of the init process with the next status
|
||||
func (p *Init) SetExited(status int) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.initState.SetExited(status)
|
||||
}
|
||||
|
||||
func (p *Init) setExited(status int) {
|
||||
p.exited = time.Now()
|
||||
p.status = status
|
||||
p.Platform.ShutdownConsole(context.Background(), p.console)
|
||||
close(p.waitBlock)
|
||||
}
|
||||
|
||||
// Delete the init process
|
||||
func (p *Init) Delete(ctx context.Context) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.initState.Delete(ctx)
|
||||
}
|
||||
|
||||
func (p *Init) delete(ctx context.Context) error {
|
||||
p.killAll(ctx)
|
||||
p.wg.Wait()
|
||||
err := p.runtime.Delete(ctx, p.id, nil)
|
||||
// ignore errors if a runtime has already deleted the process
|
||||
// but we still hold metadata and pipes
|
||||
//
|
||||
// this is common during a checkpoint, runc will delete the container state
|
||||
// after a checkpoint and the container will no longer exist within runc
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
err = nil
|
||||
} else {
|
||||
err = p.runtimeError(err, "failed to delete task")
|
||||
}
|
||||
}
|
||||
if p.io != nil {
|
||||
for _, c := range p.closers {
|
||||
c.Close()
|
||||
}
|
||||
p.io.Close()
|
||||
}
|
||||
if err2 := mount.UnmountAll(p.Rootfs, 0); err2 != nil {
|
||||
log.G(ctx).WithError(err2).Warn("failed to cleanup rootfs mount")
|
||||
if err == nil {
|
||||
err = errors.Wrap(err2, "failed rootfs umount")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Resize the init processes console
|
||||
func (p *Init) Resize(ws console.WinSize) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.console == nil {
|
||||
return nil
|
||||
}
|
||||
return p.console.Resize(ws)
|
||||
}
|
||||
|
||||
func (p *Init) resize(ws console.WinSize) error {
|
||||
if p.console == nil {
|
||||
return nil
|
||||
}
|
||||
return p.console.Resize(ws)
|
||||
}
|
||||
|
||||
// Kill the init process
|
||||
func (p *Init) Kill(ctx context.Context, signal uint32, all bool) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.initState.Kill(ctx, signal, all)
|
||||
}
|
||||
|
||||
func (p *Init) kill(context context.Context, signal uint32, all bool) error {
|
||||
var (
|
||||
killErr error
|
||||
backoff = 100 * time.Millisecond
|
||||
)
|
||||
timeout := 1 * time.Second
|
||||
for start := time.Now(); time.Now().Sub(start) < timeout; {
|
||||
c, err := p.runtime.State(context, p.id)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
return errors.Wrapf(errdefs.ErrNotFound, "no such process")
|
||||
}
|
||||
return p.runtimeError(err, "OCI runtime state failed")
|
||||
}
|
||||
// For runsc, signal only works when container is running state.
|
||||
// If the container is not in running state, directly return
|
||||
// "no such process"
|
||||
if p.convertStatus(c.Status) == "stopped" {
|
||||
return errors.Wrapf(errdefs.ErrNotFound, "no such process")
|
||||
}
|
||||
killErr = p.runtime.Kill(context, p.id, int(signal), &runsc.KillOpts{
|
||||
All: all,
|
||||
})
|
||||
if killErr == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
}
|
||||
return p.runtimeError(killErr, "kill timeout")
|
||||
}
|
||||
|
||||
// KillAll processes belonging to the init process
|
||||
func (p *Init) KillAll(context context.Context) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.killAll(context)
|
||||
}
|
||||
|
||||
func (p *Init) killAll(context context.Context) error {
|
||||
p.runtime.Kill(context, p.id, int(syscall.SIGKILL), &runsc.KillOpts{
|
||||
All: true,
|
||||
})
|
||||
// Ignore error handling for `runsc kill --all` for now.
|
||||
// * If it doesn't return error, it is good;
|
||||
// * If it returns error, consider the container has already stopped.
|
||||
// TODO: Fix `runsc kill --all` error handling.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stdin of the process
|
||||
func (p *Init) Stdin() io.Closer {
|
||||
return p.stdin
|
||||
}
|
||||
|
||||
// Runtime returns the OCI runtime configured for the init process
|
||||
func (p *Init) Runtime() *runsc.Runsc {
|
||||
return p.runtime
|
||||
}
|
||||
|
||||
// Exec returns a new child process
|
||||
func (p *Init) Exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.initState.Exec(ctx, path, r)
|
||||
}
|
||||
|
||||
// exec returns a new exec'd process
|
||||
func (p *Init) exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
// process exec request
|
||||
var spec specs.Process
|
||||
if err := json.Unmarshal(r.Spec.Value, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec.Terminal = r.Terminal
|
||||
|
||||
e := &execProcess{
|
||||
id: r.ID,
|
||||
path: path,
|
||||
parent: p,
|
||||
spec: spec,
|
||||
stdio: proc.Stdio{
|
||||
Stdin: r.Stdin,
|
||||
Stdout: r.Stdout,
|
||||
Stderr: r.Stderr,
|
||||
Terminal: r.Terminal,
|
||||
},
|
||||
waitBlock: make(chan struct{}),
|
||||
}
|
||||
e.execState = &execCreatedState{p: e}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Stdio of the process
|
||||
func (p *Init) Stdio() proc.Stdio {
|
||||
return p.stdio
|
||||
}
|
||||
|
||||
func (p *Init) runtimeError(rErr error, msg string) error {
|
||||
if rErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rMsg, err := getLastRuntimeError(p.runtime)
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrapf(rErr, "%s: %s (%s)", msg, "unable to retrieve OCI runtime error", err.Error())
|
||||
case rMsg == "":
|
||||
return errors.Wrap(rErr, msg)
|
||||
default:
|
||||
return errors.Errorf("%s: %s", msg, rMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Init) convertStatus(status string) string {
|
||||
if status == "created" && !p.Sandbox && p.status == internalErrorCode {
|
||||
// Treat start failure state for non-root container as stopped.
|
||||
return "stopped"
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func withConditionalIO(c proc.Stdio) runc.IOOpt {
|
||||
return func(o *runc.IOOption) {
|
||||
o.OpenStdin = c.Stdin != ""
|
||||
o.OpenStdout = c.Stdout != ""
|
||||
o.OpenStderr = c.Stderr != ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/runtime/proc"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type initState interface {
|
||||
Resize(console.WinSize) error
|
||||
Start(context.Context) error
|
||||
Delete(context.Context) error
|
||||
Exec(context.Context, string, *ExecConfig) (proc.Process, error)
|
||||
Kill(context.Context, uint32, bool) error
|
||||
SetExited(int)
|
||||
}
|
||||
|
||||
type createdState struct {
|
||||
p *Init
|
||||
}
|
||||
|
||||
func (s *createdState) transition(name string) error {
|
||||
switch name {
|
||||
case "running":
|
||||
s.p.initState = &runningState{p: s.p}
|
||||
case "stopped":
|
||||
s.p.initState = &stoppedState{p: s.p}
|
||||
case "deleted":
|
||||
s.p.initState = &deletedState{}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *createdState) Resize(ws console.WinSize) error {
|
||||
return s.p.resize(ws)
|
||||
}
|
||||
|
||||
func (s *createdState) Start(ctx context.Context) error {
|
||||
if err := s.p.start(ctx); err != nil {
|
||||
// Containerd doesn't allow deleting container in created state.
|
||||
// However, for gvisor, a non-root container in created state can
|
||||
// only go to running state. If the container can't be started,
|
||||
// it can only stay in created state, and never be deleted.
|
||||
// To work around that, we treat non-root container in start failure
|
||||
// state as stopped.
|
||||
if !s.p.Sandbox {
|
||||
s.p.io.Close()
|
||||
s.p.setExited(internalErrorCode)
|
||||
if err := s.transition("stopped"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.transition("running")
|
||||
}
|
||||
|
||||
func (s *createdState) Delete(ctx context.Context) error {
|
||||
if err := s.p.delete(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.transition("deleted")
|
||||
}
|
||||
|
||||
func (s *createdState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return s.p.kill(ctx, sig, all)
|
||||
}
|
||||
|
||||
func (s *createdState) SetExited(status int) {
|
||||
s.p.setExited(status)
|
||||
|
||||
if err := s.transition("stopped"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *createdState) Exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
return s.p.exec(ctx, path, r)
|
||||
}
|
||||
|
||||
type runningState struct {
|
||||
p *Init
|
||||
}
|
||||
|
||||
func (s *runningState) transition(name string) error {
|
||||
switch name {
|
||||
case "stopped":
|
||||
s.p.initState = &stoppedState{p: s.p}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *runningState) Resize(ws console.WinSize) error {
|
||||
return s.p.resize(ws)
|
||||
}
|
||||
|
||||
func (s *runningState) Start(ctx context.Context) error {
|
||||
return errors.Errorf("cannot start a running process")
|
||||
}
|
||||
|
||||
func (s *runningState) Delete(ctx context.Context) error {
|
||||
return errors.Errorf("cannot delete a running process")
|
||||
}
|
||||
|
||||
func (s *runningState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return s.p.kill(ctx, sig, all)
|
||||
}
|
||||
|
||||
func (s *runningState) SetExited(status int) {
|
||||
s.p.setExited(status)
|
||||
|
||||
if err := s.transition("stopped"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *runningState) Exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
return s.p.exec(ctx, path, r)
|
||||
}
|
||||
|
||||
type stoppedState struct {
|
||||
p *Init
|
||||
}
|
||||
|
||||
func (s *stoppedState) transition(name string) error {
|
||||
switch name {
|
||||
case "deleted":
|
||||
s.p.initState = &deletedState{}
|
||||
default:
|
||||
return errors.Errorf("invalid state transition %q to %q", stateName(s), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stoppedState) Resize(ws console.WinSize) error {
|
||||
return errors.Errorf("cannot resize a stopped container")
|
||||
}
|
||||
|
||||
func (s *stoppedState) Start(ctx context.Context) error {
|
||||
return errors.Errorf("cannot start a stopped process")
|
||||
}
|
||||
|
||||
func (s *stoppedState) Delete(ctx context.Context) error {
|
||||
if err := s.p.delete(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.transition("deleted")
|
||||
}
|
||||
|
||||
func (s *stoppedState) Kill(ctx context.Context, sig uint32, all bool) error {
|
||||
return errdefs.ToGRPCf(errdefs.ErrNotFound, "process %s not found", s.p.id)
|
||||
}
|
||||
|
||||
func (s *stoppedState) SetExited(status int) {
|
||||
// no op
|
||||
}
|
||||
|
||||
func (s *stoppedState) Exec(ctx context.Context, path string, r *ExecConfig) (proc.Process, error) {
|
||||
return nil, errors.Errorf("cannot exec in a stopped state")
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/fifo"
|
||||
runc "github.com/containerd/go-runc"
|
||||
)
|
||||
|
||||
// TODO(random-liu): This file can be a util.
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buffer := make([]byte, 32<<10)
|
||||
return &buffer
|
||||
},
|
||||
}
|
||||
|
||||
func copyPipes(ctx context.Context, rio runc.IO, stdin, stdout, stderr string, wg, cwg *sync.WaitGroup) error {
|
||||
var sameFile *countingWriteCloser
|
||||
for _, i := range []struct {
|
||||
name string
|
||||
dest func(wc io.WriteCloser, rc io.Closer)
|
||||
}{
|
||||
{
|
||||
name: stdout,
|
||||
dest: func(wc io.WriteCloser, rc io.Closer) {
|
||||
wg.Add(1)
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
if _, err := io.CopyBuffer(wc, rio.Stdout(), *p); err != nil {
|
||||
log.G(ctx).Warn("error copying stdout")
|
||||
}
|
||||
wg.Done()
|
||||
wc.Close()
|
||||
if rc != nil {
|
||||
rc.Close()
|
||||
}
|
||||
}()
|
||||
},
|
||||
}, {
|
||||
name: stderr,
|
||||
dest: func(wc io.WriteCloser, rc io.Closer) {
|
||||
wg.Add(1)
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
if _, err := io.CopyBuffer(wc, rio.Stderr(), *p); err != nil {
|
||||
log.G(ctx).Warn("error copying stderr")
|
||||
}
|
||||
wg.Done()
|
||||
wc.Close()
|
||||
if rc != nil {
|
||||
rc.Close()
|
||||
}
|
||||
}()
|
||||
},
|
||||
},
|
||||
} {
|
||||
ok, err := isFifo(i.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
fw io.WriteCloser
|
||||
fr io.Closer
|
||||
)
|
||||
if ok {
|
||||
if fw, err = fifo.OpenFifo(ctx, i.name, syscall.O_WRONLY, 0); err != nil {
|
||||
return fmt.Errorf("gvisor-containerd-shim: opening %s failed: %s", i.name, err)
|
||||
}
|
||||
if fr, err = fifo.OpenFifo(ctx, i.name, syscall.O_RDONLY, 0); err != nil {
|
||||
return fmt.Errorf("gvisor-containerd-shim: opening %s failed: %s", i.name, err)
|
||||
}
|
||||
} else {
|
||||
if sameFile != nil {
|
||||
sameFile.count++
|
||||
i.dest(sameFile, nil)
|
||||
continue
|
||||
}
|
||||
if fw, err = os.OpenFile(i.name, syscall.O_WRONLY|syscall.O_APPEND, 0); err != nil {
|
||||
return fmt.Errorf("gvisor-containerd-shim: opening %s failed: %s", i.name, err)
|
||||
}
|
||||
if stdout == stderr {
|
||||
sameFile = &countingWriteCloser{
|
||||
WriteCloser: fw,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
i.dest(fw, fr)
|
||||
}
|
||||
if stdin == "" {
|
||||
return nil
|
||||
}
|
||||
f, err := fifo.OpenFifo(context.Background(), stdin, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gvisor-containerd-shim: opening %s failed: %s", stdin, err)
|
||||
}
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
|
||||
io.CopyBuffer(rio.Stdin(), f, *p)
|
||||
rio.Stdin().Close()
|
||||
f.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// countingWriteCloser masks io.Closer() until close has been invoked a certain number of times.
|
||||
type countingWriteCloser struct {
|
||||
io.WriteCloser
|
||||
count int64
|
||||
}
|
||||
|
||||
func (c *countingWriteCloser) Close() error {
|
||||
if atomic.AddInt64(&c.count, -1) > 0 {
|
||||
return nil
|
||||
}
|
||||
return c.WriteCloser.Close()
|
||||
}
|
||||
|
||||
// isFifo checks if a file is a fifo
|
||||
// if the file does not exist then it returns false
|
||||
func isFifo(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if stat.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RunscRoot is the path to the root runsc state directory
|
||||
const RunscRoot = "/run/containerd/runsc"
|
||||
|
||||
func stateName(v interface{}) string {
|
||||
switch v.(type) {
|
||||
case *runningState, *execRunningState:
|
||||
return "running"
|
||||
case *createdState, *execCreatedState:
|
||||
return "created"
|
||||
case *deletedState:
|
||||
return "deleted"
|
||||
case *stoppedState:
|
||||
return "stopped"
|
||||
}
|
||||
panic(errors.Errorf("invalid state %v", v))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
google_protobuf "github.com/gogo/protobuf/types"
|
||||
|
||||
runc "github.com/containerd/go-runc"
|
||||
)
|
||||
|
||||
// Mount holds filesystem mount configuration
|
||||
type Mount struct {
|
||||
Type string
|
||||
Source string
|
||||
Target string
|
||||
Options []string
|
||||
}
|
||||
|
||||
// CreateConfig hold task creation configuration
|
||||
type CreateConfig struct {
|
||||
ID string
|
||||
Bundle string
|
||||
Runtime string
|
||||
Rootfs []Mount
|
||||
Terminal bool
|
||||
Stdin string
|
||||
Stdout string
|
||||
Stderr string
|
||||
Options *google_protobuf.Any
|
||||
}
|
||||
|
||||
// ExecConfig holds exec creation configuration
|
||||
type ExecConfig struct {
|
||||
ID string
|
||||
Terminal bool
|
||||
Stdin string
|
||||
Stdout string
|
||||
Stderr string
|
||||
Spec *google_protobuf.Any
|
||||
}
|
||||
|
||||
// Exit is the type of exit events
|
||||
type Exit struct {
|
||||
Timestamp time.Time
|
||||
ID string
|
||||
Status int
|
||||
}
|
||||
|
||||
// ProcessMonitor monitors process exit changes
|
||||
type ProcessMonitor interface {
|
||||
// Subscribe to process exit changes
|
||||
Subscribe() chan runc.Exit
|
||||
// Unsubscribe to process exit changes
|
||||
Unsubscribe(c chan runc.Exit)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
runsc "github.com/google/gvisor-containerd-shim/pkg/go-runsc"
|
||||
)
|
||||
|
||||
const (
|
||||
internalErrorCode = 128
|
||||
bufferSize = 32
|
||||
)
|
||||
|
||||
// ExitCh is the exit events channel for containers and exec processes
|
||||
// inside the sandbox.
|
||||
var ExitCh = make(chan Exit, bufferSize)
|
||||
|
||||
// TODO(random-liu): This can be a utility.
|
||||
|
||||
// TODO(mlaventure): move to runc package?
|
||||
func getLastRuntimeError(r *runsc.Runsc) (string, error) {
|
||||
if r.Log == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(r.Log, os.O_RDONLY, 0400)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var (
|
||||
errMsg string
|
||||
log struct {
|
||||
Level string
|
||||
Msg string
|
||||
Time time.Time
|
||||
}
|
||||
)
|
||||
|
||||
dec := json.NewDecoder(f)
|
||||
for err = nil; err == nil; {
|
||||
if err = dec.Decode(&log); err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
if log.Level == "error" {
|
||||
errMsg = strings.TrimSpace(log.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
return errMsg, nil
|
||||
}
|
||||
|
||||
func copyFile(to, from string) error {
|
||||
ff, err := os.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ff.Close()
|
||||
tt, err := os.Create(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tt.Close()
|
||||
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
_, err = io.CopyBuffer(tt, ff, *p)
|
||||
return err
|
||||
}
|
||||
|
||||
func hasNoIO(r *CreateConfig) bool {
|
||||
return r.Stdin == "" && r.Stdout == "" && r.Stderr == ""
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2019 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package shim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/fifo"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type linuxPlatform struct {
|
||||
epoller *console.Epoller
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) CopyConsole(ctx context.Context, console console.Console, stdin, stdout, stderr string, wg, cwg *sync.WaitGroup) (console.Console, error) {
|
||||
if p.epoller == nil {
|
||||
return nil, errors.New("uninitialized epoller")
|
||||
}
|
||||
|
||||
epollConsole, err := p.epoller.Add(console)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stdin != "" {
|
||||
in, err := fifo.OpenFifo(ctx, stdin, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
io.CopyBuffer(epollConsole, in, *p)
|
||||
}()
|
||||
}
|
||||
|
||||
outw, err := fifo.OpenFifo(ctx, stdout, syscall.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outr, err := fifo.OpenFifo(ctx, stdout, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wg.Add(1)
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
io.CopyBuffer(outw, epollConsole, *p)
|
||||
epollConsole.Close()
|
||||
outr.Close()
|
||||
outw.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
return epollConsole, nil
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) ShutdownConsole(ctx context.Context, cons console.Console) error {
|
||||
if p.epoller == nil {
|
||||
return errors.New("uninitialized epoller")
|
||||
}
|
||||
epollConsole, ok := cons.(*console.EpollConsole)
|
||||
if !ok {
|
||||
return errors.Errorf("expected EpollConsole, got %#v", cons)
|
||||
}
|
||||
return epollConsole.Shutdown(p.epoller.CloseConsole)
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) Close() error {
|
||||
return p.epoller.Close()
|
||||
}
|
||||
|
||||
// initialize a single epoll fd to manage our consoles. `initPlatform` should
|
||||
// only be called once.
|
||||
func (s *Service) initPlatform() error {
|
||||
if s.platform != nil {
|
||||
return nil
|
||||
}
|
||||
epoller, err := console.NewEpoller()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize epoller")
|
||||
}
|
||||
s.platform = &linuxPlatform{
|
||||
epoller: epoller,
|
||||
}
|
||||
go epoller.Wait()
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containerd/cri/pkg/annotations"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// ReadSpec reads OCI spec from the bundle directory.
|
||||
func ReadSpec(bundle string) (*specs.Spec, error) {
|
||||
f, err := os.Open(filepath.Join(bundle, "config.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var spec specs.Spec
|
||||
if err := json.Unmarshal(b, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// IsSandbox checks whether a container is a sandbox container.
|
||||
func IsSandbox(spec *specs.Spec) bool {
|
||||
t, ok := spec.Annotations[annotations.ContainerType]
|
||||
return !ok || t == annotations.ContainerTypeSandbox
|
||||
}
|
||||
|
||||
// UserLogPath gets user log path from OCI annotation.
|
||||
func UserLogPath(spec *specs.Spec) string {
|
||||
sandboxLogDir := spec.Annotations[annotations.SandboxLogDir]
|
||||
if sandboxLogDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(sandboxLogDir, "gvisor.log")
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/cri/pkg/annotations"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const volumeKeyPrefix = "dev.gvisor.spec.mount."
|
||||
|
||||
var kubeletPodsDir = "/var/lib/kubelet/pods"
|
||||
|
||||
// volumeName gets volume name from volume annotation key, example:
|
||||
// dev.gvisor.spec.mount.NAME.share
|
||||
func volumeName(k string) string {
|
||||
return strings.SplitN(strings.TrimPrefix(k, volumeKeyPrefix), ".", 2)[0]
|
||||
}
|
||||
|
||||
// volumeFieldName gets volume field name from volume annotation key, example:
|
||||
// `type` is the field of dev.gvisor.spec.mount.NAME.type
|
||||
func volumeFieldName(k string) string {
|
||||
parts := strings.Split(strings.TrimPrefix(k, volumeKeyPrefix), ".")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// podUID gets pod UID from the pod log path.
|
||||
func podUID(s *specs.Spec) (string, error) {
|
||||
sandboxLogDir := s.Annotations[annotations.SandboxLogDir]
|
||||
if sandboxLogDir == "" {
|
||||
return "", errors.New("no sandbox log path annotation")
|
||||
}
|
||||
fields := strings.Split(filepath.Base(sandboxLogDir), "_")
|
||||
switch len(fields) {
|
||||
case 1: // This is the old CRI logging path
|
||||
return fields[0], nil
|
||||
case 3: // This is the new CRI logging path
|
||||
return fields[2], nil
|
||||
}
|
||||
return "", errors.Errorf("unexpected sandbox log path %q", sandboxLogDir)
|
||||
}
|
||||
|
||||
// isVolumeKey checks whether an annotation key is for volume.
|
||||
func isVolumeKey(k string) bool {
|
||||
return strings.HasPrefix(k, volumeKeyPrefix)
|
||||
}
|
||||
|
||||
// volumeSourceKey constructs the annotation key for volume source.
|
||||
func volumeSourceKey(volume string) string {
|
||||
return volumeKeyPrefix + volume + ".source"
|
||||
}
|
||||
|
||||
// volumePath searches the volume path in the kubelet pod directory.
|
||||
func volumePath(volume, uid string) (string, error) {
|
||||
// TODO: Support subpath when gvisor supports pod volume bind mount.
|
||||
volumeSearchPath := fmt.Sprintf("%s/%s/volumes/*/%s", kubeletPodsDir, uid, volume)
|
||||
dirs, err := filepath.Glob(volumeSearchPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(dirs) != 1 {
|
||||
return "", errors.Errorf("unexpected matched volume list %v", dirs)
|
||||
}
|
||||
return dirs[0], nil
|
||||
}
|
||||
|
||||
// isVolumePath checks whether a string is the volume path.
|
||||
func isVolumePath(volume, path string) (bool, error) {
|
||||
// TODO: Support subpath when gvisor supports pod volume bind mount.
|
||||
volumeSearchPath := fmt.Sprintf("%s/*/volumes/*/%s", kubeletPodsDir, volume)
|
||||
return filepath.Match(volumeSearchPath, path)
|
||||
}
|
||||
|
||||
// UpdateVolumeAnnotations add necessary OCI annotations for gvisor
|
||||
// volume optimization.
|
||||
func UpdateVolumeAnnotations(bundle string, s *specs.Spec) error {
|
||||
var (
|
||||
uid string
|
||||
err error
|
||||
)
|
||||
if IsSandbox(s) {
|
||||
uid, err = podUID(s)
|
||||
if err != nil {
|
||||
// Skip if we can't get pod UID, because this doesn't work
|
||||
// for containerd 1.1.
|
||||
logrus.WithError(err).Error("Can't get pod uid")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var updated bool
|
||||
for k, v := range s.Annotations {
|
||||
if !isVolumeKey(k) {
|
||||
continue
|
||||
}
|
||||
if volumeFieldName(k) != "type" {
|
||||
continue
|
||||
}
|
||||
volume := volumeName(k)
|
||||
if uid != "" {
|
||||
// This is a sandbox
|
||||
path, err := volumePath(volume, uid)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "get volume path for %q", volume)
|
||||
}
|
||||
s.Annotations[volumeSourceKey(volume)] = path
|
||||
updated = true
|
||||
} else {
|
||||
// This is a container
|
||||
for i := range s.Mounts {
|
||||
// An error is returned for sandbox if source annotation
|
||||
// is not successfully applied, so it is guaranteed that
|
||||
// the source annotation for sandbox has already been
|
||||
// successfully applied at this point.
|
||||
// The volume name is unique inside a pod, so matching without
|
||||
// podUID is fine here.
|
||||
// TODO: Pass podUID down to shim for containers to do
|
||||
// more accurate matching.
|
||||
if yes, _ := isVolumePath(volume, s.Mounts[i].Source); yes {
|
||||
// gVisor requires the container mount type to match
|
||||
// sandbox mount type.
|
||||
s.Mounts[i].Type = v
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
// Update bundle
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filepath.Join(bundle, "config.json"), b, 0666)
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// Copyright 2019 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/cri/pkg/annotations"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
func TestUpdateVolumeAnnotations(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-update-volume-annotations")
|
||||
if err != nil {
|
||||
t.Fatalf("create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
kubeletPodsDir = dir
|
||||
|
||||
const (
|
||||
testPodUID = "testuid"
|
||||
testVolumeName = "testvolume"
|
||||
testLogDirPath = "/var/log/pods/testns_testname_" + testPodUID
|
||||
testLegacyLogDirPath = "/var/log/pods/" + testPodUID
|
||||
)
|
||||
testVolumePath := fmt.Sprintf("%s/%s/volumes/kubernetes.io~empty-dir/%s", dir, testPodUID, testVolumeName)
|
||||
|
||||
if err := os.MkdirAll(testVolumePath, 0755); err != nil {
|
||||
t.Fatalf("Create test volume: %v", err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
spec *specs.Spec
|
||||
expected *specs.Spec
|
||||
expectErr bool
|
||||
expectUpdate bool
|
||||
}{
|
||||
{
|
||||
desc: "volume annotations for sandbox",
|
||||
spec: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".source": testVolumePath,
|
||||
},
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
desc: "volume annotations for sandbox with legacy log path",
|
||||
spec: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLegacyLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLegacyLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".source": testVolumePath,
|
||||
},
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
desc: "tmpfs: volume annotations for container",
|
||||
spec: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "bind",
|
||||
Source: testVolumePath,
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/random",
|
||||
Type: "bind",
|
||||
Source: "/random",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "tmpfs",
|
||||
Source: testVolumePath,
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/random",
|
||||
Type: "bind",
|
||||
Source: "/random",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
desc: "bind: volume annotations for container",
|
||||
spec: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "bind",
|
||||
Source: testVolumePath,
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "container",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "bind",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "bind",
|
||||
Source: testVolumePath,
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "container",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "bind",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
desc: "should not return error without pod log directory",
|
||||
spec: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".share": "pod",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".type": "tmpfs",
|
||||
"dev.gvisor.spec.mount." + testVolumeName + ".options": "ro",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should return error if volume path does not exist",
|
||||
spec: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
"dev.gvisor.spec.mount.notexist.share": "pod",
|
||||
"dev.gvisor.spec.mount.notexist.type": "tmpfs",
|
||||
"dev.gvisor.spec.mount.notexist.options": "ro",
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
desc: "no volume annotations for sandbox",
|
||||
spec: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Annotations: map[string]string{
|
||||
annotations.SandboxLogDir: testLogDirPath,
|
||||
annotations.ContainerType: annotations.ContainerTypeSandbox,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no volume annotations for container",
|
||||
spec: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "bind",
|
||||
Source: "/test",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/random",
|
||||
Type: "bind",
|
||||
Source: "/random",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
},
|
||||
},
|
||||
expected: &specs.Spec{
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/test",
|
||||
Type: "bind",
|
||||
Source: "/test",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/random",
|
||||
Type: "bind",
|
||||
Source: "/random",
|
||||
Options: []string{"ro"},
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
annotations.ContainerType: annotations.ContainerTypeContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
bundle, err := ioutil.TempDir(dir, "test-bundle")
|
||||
if err != nil {
|
||||
t.Fatalf("Create test bundle: %v", err)
|
||||
}
|
||||
err = UpdateVolumeAnnotations(bundle, test.spec)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, but got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expected, test.spec) {
|
||||
t.Fatalf("Expected %+v, got %+v", test.expected, test.spec)
|
||||
}
|
||||
if test.expectUpdate {
|
||||
b, err := ioutil.ReadFile(filepath.Join(bundle, "config.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Read spec from bundle: %v", err)
|
||||
}
|
||||
var spec specs.Spec
|
||||
if err := json.Unmarshal(b, &spec); err != nil {
|
||||
t.Fatalf("Unmarshal spec: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expected, &spec) {
|
||||
t.Fatalf("Expected %+v, got %+v", test.expected, &spec)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build linux
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/cgroups"
|
||||
eventstypes "github.com/containerd/containerd/api/events"
|
||||
"github.com/containerd/containerd/events"
|
||||
"github.com/containerd/containerd/runtime"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func newOOMEpoller(publisher events.Publisher) (*epoller, error) {
|
||||
fd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &epoller{
|
||||
fd: fd,
|
||||
publisher: publisher,
|
||||
set: make(map[uintptr]*item),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type epoller struct {
|
||||
mu sync.Mutex
|
||||
|
||||
fd int
|
||||
publisher events.Publisher
|
||||
set map[uintptr]*item
|
||||
}
|
||||
|
||||
type item struct {
|
||||
id string
|
||||
cg cgroups.Cgroup
|
||||
}
|
||||
|
||||
func (e *epoller) Close() error {
|
||||
return unix.Close(e.fd)
|
||||
}
|
||||
|
||||
func (e *epoller) run(ctx context.Context) {
|
||||
var events [128]unix.EpollEvent
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
e.Close()
|
||||
return
|
||||
default:
|
||||
n, err := unix.EpollWait(e.fd, events[:], -1)
|
||||
if err != nil {
|
||||
if err == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
logrus.WithError(err).Error("cgroups: epoll wait")
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
e.process(ctx, uintptr(events[i].Fd))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *epoller) add(id string, cg cgroups.Cgroup) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
fd, err := cg.OOMEventFD()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.set[fd] = &item{
|
||||
id: id,
|
||||
cg: cg,
|
||||
}
|
||||
event := unix.EpollEvent{
|
||||
Fd: int32(fd),
|
||||
Events: unix.EPOLLHUP | unix.EPOLLIN | unix.EPOLLERR,
|
||||
}
|
||||
return unix.EpollCtl(e.fd, unix.EPOLL_CTL_ADD, int(fd), &event)
|
||||
}
|
||||
|
||||
func (e *epoller) process(ctx context.Context, fd uintptr) {
|
||||
flush(fd)
|
||||
e.mu.Lock()
|
||||
i, ok := e.set[fd]
|
||||
if !ok {
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
e.mu.Unlock()
|
||||
if i.cg.State() == cgroups.Deleted {
|
||||
e.mu.Lock()
|
||||
delete(e.set, fd)
|
||||
e.mu.Unlock()
|
||||
unix.Close(int(fd))
|
||||
return
|
||||
}
|
||||
if err := e.publisher.Publish(ctx, runtime.TaskOOMEventTopic, &eventstypes.TaskOOM{
|
||||
ContainerID: i.id,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("publish OOM event")
|
||||
}
|
||||
}
|
||||
|
||||
func flush(fd uintptr) error {
|
||||
var buf [8]byte
|
||||
_, err := unix.Read(int(fd), buf[:])
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package options
|
||||
|
||||
const OptionType = "io.containerd.runsc.v1.options"
|
||||
|
||||
// Options is runtime options for io.containerd.runsc.v1.
|
||||
type Options struct {
|
||||
// ShimCgroup is the cgroup the shim should be in.
|
||||
ShimCgroup string `toml:"shim_cgroup"`
|
||||
// IoUid is the I/O's pipes uid.
|
||||
IoUid uint32 `toml:"io_uid"`
|
||||
// IoUid is the I/O's pipes gid.
|
||||
IoGid uint32 `toml:"io_gid"`
|
||||
// BinaryName is the binary name of the runsc binary.
|
||||
BinaryName string `toml:"binary_name"`
|
||||
// Root is the runsc root directory.
|
||||
Root string `toml:"root"`
|
||||
// RunscConfig is a key/value map of all runsc flags.
|
||||
RunscConfig map[string]string `toml:"runsc_config"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,112 @@
|
||||
// Copyright 2018 The containerd Authors.
|
||||
// Copyright 2018 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build linux
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/fifo"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type linuxPlatform struct {
|
||||
epoller *console.Epoller
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) CopyConsole(ctx context.Context, console console.Console, stdin, stdout, stderr string, wg, cwg *sync.WaitGroup) (console.Console, error) {
|
||||
if p.epoller == nil {
|
||||
return nil, errors.New("uninitialized epoller")
|
||||
}
|
||||
|
||||
epollConsole, err := p.epoller.Add(console)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stdin != "" {
|
||||
in, err := fifo.OpenFifo(context.Background(), stdin, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
io.CopyBuffer(epollConsole, in, *p)
|
||||
}()
|
||||
}
|
||||
|
||||
outw, err := fifo.OpenFifo(ctx, stdout, syscall.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outr, err := fifo.OpenFifo(ctx, stdout, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wg.Add(1)
|
||||
cwg.Add(1)
|
||||
go func() {
|
||||
cwg.Done()
|
||||
p := bufPool.Get().(*[]byte)
|
||||
defer bufPool.Put(p)
|
||||
io.CopyBuffer(outw, epollConsole, *p)
|
||||
epollConsole.Close()
|
||||
outr.Close()
|
||||
outw.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
return epollConsole, nil
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) ShutdownConsole(ctx context.Context, cons console.Console) error {
|
||||
if p.epoller == nil {
|
||||
return errors.New("uninitialized epoller")
|
||||
}
|
||||
epollConsole, ok := cons.(*console.EpollConsole)
|
||||
if !ok {
|
||||
return errors.Errorf("expected EpollConsole, got %#v", cons)
|
||||
}
|
||||
return epollConsole.Shutdown(p.epoller.CloseConsole)
|
||||
}
|
||||
|
||||
func (p *linuxPlatform) Close() error {
|
||||
return p.epoller.Close()
|
||||
}
|
||||
|
||||
// initialize a single epoll fd to manage our consoles. `initPlatform` should
|
||||
// only be called once.
|
||||
func (s *service) initPlatform() error {
|
||||
if s.platform != nil {
|
||||
return nil
|
||||
}
|
||||
epoller, err := console.NewEpoller()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize epoller")
|
||||
}
|
||||
s.platform = &linuxPlatform{
|
||||
epoller: epoller,
|
||||
}
|
||||
go epoller.Wait()
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user