Files
virtual-kubelet/vendor/github.com/vmware/vic/lib/tether/tether.go
Loc Nguyen 513cebe7b7 VMware vSphere Integrated Containers provider (#206)
* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
2018-06-04 15:41:32 -07:00

1105 lines
29 KiB
Go

// Copyright 2016-2018 VMware, Inc. All Rights Reserved.
//
// 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
//
// http://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 tether
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
_ "net/http/pprof" // allow enabling pprof in containerVM
"os"
"os/exec"
"os/signal"
"path"
"runtime/debug"
"sort"
"sync"
"syscall"
"time"
"golang.org/x/crypto/ssh"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/system"
"github.com/vmware/vic/lib/tether/msgs"
"github.com/vmware/vic/lib/tether/shared"
"github.com/vmware/vic/pkg/dio"
"github.com/vmware/vic/pkg/log/syslog"
"github.com/vmware/vic/pkg/serial"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/opencontainers/runtime-spec/specs-go"
)
const (
// MaxDeathRecords The maximum number of records to keep for restarting processes
MaxDeathRecords = 5
// the length of a truncated ID for use as hostname
shortLen = 12
)
// Sys is used to configure where the target system files are
var (
// Used to access the acutal system paths and files
Sys = shared.Sys
// Used to access and manipulate the tether modified bind sources
// that are mounted over the system ones.
BindSys = system.NewWithRoot("/.tether")
once sync.Once
)
const (
useRunc = true
runcPath = "/sbin/runc"
)
type tether struct {
// the implementation to use for tailored operations
ops Operations
// the reload channel is used to block reloading of the config
reload chan struct{}
// config holds the main configuration for the executor
config *ExecutorConfig
// a set of extensions that get to operate on the config
extensions map[string]Extension
src extraconfig.DataSource
sink extraconfig.DataSink
// Cancelable context and its cancel func.
ctx context.Context
cancel context.CancelFunc
incoming chan os.Signal
// syslog writer shared by all sessions
writer syslog.Writer
// used for running vm initialization logic once in the reload loop
initialize sync.Once
}
func New(src extraconfig.DataSource, sink extraconfig.DataSink, ops Operations) Tether {
ctx, cancel := context.WithCancel(context.Background())
return &tether{
ops: ops,
reload: make(chan struct{}, 1),
config: &ExecutorConfig{
pids: make(map[int]*SessionConfig),
},
extensions: make(map[string]Extension),
src: src,
sink: sink,
ctx: ctx,
cancel: cancel,
incoming: make(chan os.Signal, 32),
}
}
// removeChildPid is a synchronized accessor for the pid map the deletes the entry and returns the value
func (t *tether) removeChildPid(pid int) (*SessionConfig, bool) {
t.config.pidMutex.Lock()
defer t.config.pidMutex.Unlock()
session, ok := t.config.pids[pid]
delete(t.config.pids, pid)
return session, ok
}
// lenChildPid returns the number of entries
func (t *tether) lenChildPid() int {
t.config.pidMutex.Lock()
defer t.config.pidMutex.Unlock()
return len(t.config.pids)
}
func (t *tether) setup() error {
defer trace.End(trace.Begin("main tether setup"))
// set up tether logging destination
out, err := t.ops.Log()
if err != nil {
log.Errorf("failed to open tether log: %s", err)
return err
}
if out != nil {
log.SetOutput(out)
}
t.reload = make(chan struct{}, 1)
t.config = &ExecutorConfig{
pids: make(map[int]*SessionConfig),
}
if err := t.childReaper(); err != nil {
log.Errorf("Failed to start reaper %s", err)
return err
}
if err := t.ops.Setup(t); err != nil {
log.Errorf("Failed tether setup: %s", err)
return err
}
for name, ext := range t.extensions {
log.Infof("Starting extension %s", name)
err := ext.Start()
if err != nil {
log.Errorf("Failed to start extension %s: %s", name, err)
return err
}
}
pidDir := shared.PIDFileDir()
// #nosec: Expect directory permissions to be 0700 or less
if err = os.MkdirAll(pidDir, 0755); err != nil {
log.Errorf("could not create pid file directory %s: %s", pidDir, err)
}
// Create PID file for tether
tname := path.Base(os.Args[0])
err = ioutil.WriteFile(fmt.Sprintf("%s.pid", path.Join(pidDir, tname)),
[]byte(fmt.Sprintf("%d", os.Getpid())),
0644)
if err != nil {
log.Errorf("Unable to open PID file for %s : %s", os.Args[0], err)
}
// seed the incoming channel once to trigger child reaper. This is required to collect the zombies created by switch-root
t.triggerReaper()
return nil
}
func (t *tether) cleanup() {
defer trace.End(trace.Begin("main tether cleanup"))
// stop child reaping
t.stopReaper()
// stop the extensions first as they may use the config
for name, ext := range t.extensions {
log.Infof("Stopping extension %s", name)
err := ext.Stop()
if err != nil {
log.Warnf("Failed to cleanly stop extension %s", name)
}
}
// return logging to standard location
log.SetOutput(os.Stdout)
// perform basic cleanup
t.ops.Cleanup()
}
func (t *tether) setLogLevel() {
// TODO: move all of this into an extension.Pre() block when we move to that model
// adjust the logging level appropriately
log.SetLevel(log.InfoLevel)
// TODO: do not echo application output to console without debug enabled
serial.DisableTracing()
if t.config.DebugLevel > 0 {
log.SetLevel(log.DebugLevel)
logConfig(t.config)
}
if t.config.DebugLevel > 1 {
serial.EnableTracing()
log.Info("Launching pprof server on port 6060")
fn := func() {
go http.ListenAndServe("0.0.0.0:6060", nil)
}
once.Do(fn)
}
if t.config.DebugLevel > 3 {
// extraconfig is very, very verbose
extraconfig.SetLogLevel(log.DebugLevel)
}
}
func (t *tether) setHostname() error {
short := t.config.ID
if len(short) > shortLen {
short = short[:shortLen]
}
full := t.config.Hostname
if t.config.Hostname != "" && t.config.Domainname != "" {
full = fmt.Sprintf("%s.%s", t.config.Hostname, t.config.Domainname)
}
hostname := short
if full != "" {
hostname = full
}
if err := t.ops.SetHostname(hostname, t.config.Name); err != nil {
// we don't attempt to recover from this - it's a fundamental misconfiguration
// so just exit
return fmt.Errorf("failed to set hostname: %s", err)
}
return nil
}
func (t *tether) setNetworks() error {
// internal networks must be applied first
for _, network := range t.config.Networks {
if !network.Internal {
continue
}
if err := t.ops.Apply(network); err != nil {
return fmt.Errorf("failed to apply network endpoint config: %s", err)
}
}
for _, network := range t.config.Networks {
if network.Internal {
continue
}
if err := t.ops.Apply(network); err != nil {
return fmt.Errorf("failed to apply network endpoint config: %s", err)
}
}
return nil
}
func (t *tether) setMounts() error {
// provides a lookup from path to volume reference.
pathIndex := make(map[string]string, 0)
mounts := make([]string, 0, len(t.config.Mounts))
for k, v := range t.config.Mounts {
log.Infof("** mount = %#v", v)
mounts = append(mounts, v.Path)
pathIndex[v.Path] = k
}
// Order the mount paths so that we are doing them in shortest order first.
sort.Strings(mounts)
for _, v := range mounts {
targetRef := pathIndex[v]
mountTarget := t.config.Mounts[targetRef]
switch mountTarget.Source.Scheme {
case "label":
// this could block indefinitely while waiting for a volume to present
// return error if mount volume failed, to fail container start
if err := t.ops.MountLabel(context.Background(), mountTarget.Source.Path, mountTarget.Path); err != nil {
return err
}
case "nfs":
// return error if mount nfs volume failed, to fail container start, so user knows there is something wrong
if err := t.ops.MountTarget(context.Background(), mountTarget.Source, mountTarget.Path, mountTarget.Mode); err != nil {
return err
}
default:
return fmt.Errorf("unsupported volume mount type for %s: %s", targetRef, mountTarget.Source.Scheme)
}
}
// FIXME: populateVolumes() does not handle the nested volume case properly.
return t.populateVolumes()
}
func (t *tether) populateVolumes() error {
defer trace.End(trace.Begin(fmt.Sprintf("populateVolumes")))
// skip if no mounts present
if len(t.config.Mounts) == 0 {
return nil
}
for _, mnt := range t.config.Mounts {
if mnt.Path == "" {
continue
}
if mnt.CopyMode == executor.CopyNew {
err := t.ops.CopyExistingContent(mnt.Path)
if err != nil {
log.Errorf("error copyExistingContent for mount %s: %+v", mnt.Path, err)
return err
}
}
}
return nil
}
func (t *tether) initializeSessions() error {
maps := map[string]map[string]*SessionConfig{
"Sessions": t.config.Sessions,
"Execs": t.config.Execs,
}
// we need to iterate over both sessions and execs
for name, m := range maps {
// Iterate over the Sessions and initialize them if needed
for id, session := range m {
log.Debugf("Initializing session %s", id)
session.Lock()
if session.wait != nil {
log.Warnf("Session %s already initialized", id)
} else {
if session.RunBlock {
log.Infof("Session %s wants attach capabilities. Creating its channel", id)
session.ClearToLaunch = make(chan struct{})
}
// this will need altering if tether should be capable of being restarted itself
session.Started = ""
session.StopTime = 0
session.wait = &sync.WaitGroup{}
session.extraconfigKey = name
err := t.loggingLocked(session)
if err != nil {
log.Errorf("initializing logging for session failed with %s", err)
session.Unlock()
return err
}
}
session.Unlock()
}
}
return nil
}
func (t *tether) reloadExtensions() error {
// reload the extensions
for name, ext := range t.extensions {
log.Debugf("Passing config to %s", name)
err := ext.Reload(t.config)
if err != nil {
return fmt.Errorf("Failed to cleanly reload config for extension %s: %s", name, err)
}
}
return nil
}
func (t *tether) processSessions() error {
type results struct {
id string
path string
err error
fatal bool
}
// so that we can launch multiple sessions in parallel
var wg sync.WaitGroup
// to collect the errors back from them
resultsCh := make(chan results, len(t.config.Sessions)+len(t.config.Execs))
maps := []struct {
sessions map[string]*SessionConfig
fatal bool
}{
{t.config.Sessions, true},
{t.config.Execs, false},
}
// we need to iterate over both sessions and execs
for i := range maps {
m := maps[i]
// process the sessions and launch if needed
for id, session := range m.sessions {
func() {
session.Lock()
defer session.Unlock()
log.Debugf("Processing config for session: %s", id)
var proc = session.Cmd.Process
// check if session is alive and well
if proc != nil && proc.Signal(syscall.Signal(0)) == nil {
log.Debugf("Process for session %s is running (pid: %d)", id, proc.Pid)
if !session.Active {
// stop process - for now this doesn't do any staged levels of aggression
log.Infof("Running session %s has been deactivated (pid: %d)", id, proc.Pid)
killHelper(session)
}
return
}
// if we're not activating this session and it's not running, then skip
if !session.Active {
log.Debugf("Skipping inactive session: %s", id)
return
}
priorLaunch := proc != nil || session.Started != ""
if priorLaunch && !session.Restart {
log.Debugf("Skipping non-restartable exited or failed session: %s", id)
return
}
if !priorLaunch {
log.Infof("Launching process for session %s", id)
log.Debugf("Launch failures are fatal: %t", m.fatal)
} else {
session.Diagnostics.ResurrectionCount++
// FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not
// currently sure how to expose it neatly via a utility function
extraconfig.EncodeWithPrefix(t.sink, session, extraconfig.CalculateKeys(t.config, fmt.Sprintf("%s.%s", session.extraconfigKey, id), "")[0])
log.Warnf("Re-launching process for session %s (count: %d)", id, session.Diagnostics.ResurrectionCount)
session.Cmd = *restartableCmd(&session.Cmd)
}
wg.Add(1)
go func(session *SessionConfig) {
defer wg.Done()
resultsCh <- results{
id: session.ID,
path: session.Cmd.Path,
err: t.launch(session),
fatal: m.fatal,
}
}(session)
}()
}
}
wg.Wait()
// close the channel
close(resultsCh)
// iterate over the results
for result := range resultsCh {
if result.err != nil {
detail := fmt.Errorf("failed to launch %s for %s: %s", result.path, result.id, result.err)
if result.fatal {
log.Error(detail)
return detail
}
log.Warn(detail)
return nil
}
}
return nil
}
type TetherKey struct{}
func (t *tether) Start() error {
defer trace.End(trace.Begin("main tether loop"))
defer func() {
e := recover()
if e != nil {
log.Errorf("Panic in main tether loop: %s: %s", e, debug.Stack())
// continue panicing now it's logged
panic(e)
}
}()
// do the initial setup and start the extensions
if err := t.setup(); err != nil {
log.Errorf("Failed to run setup: %s", err)
return err
}
defer t.cleanup()
// initial entry, so seed this
t.reload <- struct{}{}
for range t.reload {
var err error
select {
case <-t.ctx.Done():
log.Warnf("Someone called shutdown, returning from start")
return nil
default:
}
log.Info("Loading main configuration")
// load the config - this modifies the structure values in place
t.config.Lock()
extraconfig.Decode(t.src, t.config)
t.config.Unlock()
t.setLogLevel()
// TODO: this ensures that we run vm related setup code once
// This is temporary as none of those functions are idempotent at this point
// https://github.com/vmware/vic/issues/5833
t.initialize.Do(func() {
if err = t.setHostname(); err != nil {
return
}
// process the networks then publish any dynamic data
if err = t.setNetworks(); err != nil {
return
}
extraconfig.Encode(t.sink, t.config)
// setup the firewall
if err = retryOnError(func() error { return t.ops.SetupFirewall(t.ctx, t.config) }, 5); err != nil {
err = fmt.Errorf("Couldn't set up container-network firewall: %v", err)
return
}
//process the filesystem mounts - this is performed after networks to allow for network mounts
if err = t.setMounts(); err != nil {
return
}
})
if err != nil {
log.Error(err)
return err
}
if err = t.initializeSessions(); err != nil {
log.Error(err)
return err
}
// Danger, Will Robinson! There is a strict ordering here.
// We need to start attach server first so that it can unblock the session
if err = t.reloadExtensions(); err != nil {
log.Error(err)
return err
}
if err = t.processSessions(); err != nil {
log.Error(err)
return err
}
}
log.Info("Finished processing sessions")
return nil
}
func (t *tether) Stop() error {
defer trace.End(trace.Begin(""))
// cancel the context to signal waiters
t.cancel()
// TODO: kill all the children
close(t.reload)
return nil
}
func (t *tether) Reload() {
defer trace.End(trace.Begin(""))
select {
case <-t.ctx.Done():
log.Warnf("Someone called shutdown, dropping the reload request")
return
default:
t.reload <- struct{}{}
}
}
func (t *tether) Register(name string, extension Extension) {
log.Infof("Registering tether extension " + name)
t.extensions[name] = extension
}
func retryOnError(cmd func() error, maximumAttempts int) error {
for i := 0; i < maximumAttempts-1; i++ {
if err := cmd(); err != nil {
log.Warningf("Failed with error \"%v\". Retrying (Attempt %v).", err, i+1)
} else {
return nil
}
}
return cmd()
}
// cleanupSession performs some common cleanup work between handling a session exit and
// handling a failure to launch
// caller needs to hold session Lock
func (t *tether) cleanupSession(session *SessionConfig) {
// close down the outputs
log.Debugf("Calling close on writers")
if session.Outwriter != nil {
if err := session.Outwriter.Close(); err != nil {
log.Warnf("Close for Outwriter returned %s", err)
}
}
// this is a little ugly, however ssh channel.Close will get invoked by these calls,
// whereas CloseWrite will be invoked by the OutWriter.Close so that goes first.
if session.Errwriter != nil {
if err := session.Errwriter.Close(); err != nil {
log.Warnf("Close for Errwriter returned %s", err)
}
}
// if we're calling this we don't care about truncation of pending input, so this is
// called last
if session.Reader != nil {
log.Debugf("Calling close on reader")
if err := session.Reader.Close(); err != nil {
log.Warnf("Close for Reader returned %s", err)
}
}
// close the signaling channel (it is nil for detached sessions) and set it to nil (for restart)
if session.ClearToLaunch != nil {
log.Debugf("Calling close chan: %s", session.ID)
close(session.ClearToLaunch)
session.ClearToLaunch = nil
// reset Runblock to unblock process start next time
session.RunBlock = false
}
}
// handleSessionExit processes the result from the session command, records it in persistent
// maner and determines if the Executor should exit
// caller needs to hold session Lock
func (t *tether) handleSessionExit(session *SessionConfig) {
defer trace.End(trace.Begin("handling exit of session " + session.ID))
log.Debugf("Waiting on session.wait")
session.wait.Wait()
log.Debugf("Wait on session.wait completed")
log.Debugf("Calling wait on cmd")
if err := session.Cmd.Wait(); err != nil {
// we expect this to get an error because the child reaper will have gathered it
log.Debugf("Wait returned %s", err)
}
t.cleanupSession(session)
// Remove associated PID file
cmdname := path.Base(session.Cmd.Path)
_ = os.Remove(fmt.Sprintf("%s.pid", path.Join(shared.PIDFileDir(), cmdname)))
// set the stop time
session.StopTime = time.Now().UTC().Unix()
// this returns an arbitrary closure for invocation after the session status update
f := t.ops.HandleSessionExit(t.config, session)
extraconfig.EncodeWithPrefix(t.sink, session, extraconfig.CalculateKeys(t.config, fmt.Sprintf("%s.%s", session.extraconfigKey, session.ID), "")[0])
if f != nil {
log.Debugf("Calling t.ops.HandleSessionExit")
f()
}
}
func (t *tether) loggingLocked(session *SessionConfig) error {
stdout, stderr, err := t.ops.SessionLog(session)
if err != nil {
detail := fmt.Errorf("failed to get log writer for session: %s", err)
session.Started = detail.Error()
return detail
}
session.Outwriter = stdout
session.Errwriter = stderr
session.Reader = dio.MultiReader()
if session.Diagnostics.SysLogConfig != nil {
cfg := session.Diagnostics.SysLogConfig
var w syslog.Writer
if t.writer == nil {
t.writer, err = syslog.Dial(cfg.Network, cfg.RAddr, syslog.Info|syslog.Daemon, fmt.Sprintf("%s", t.config.ID[:shortLen]))
if err != nil {
log.Warnf("could not connect to syslog server: %s", err)
}
w = t.writer
} else {
w = t.writer.WithTag(fmt.Sprintf("%s", t.config.ID[:shortLen]))
}
if w != nil {
stdout.Add(w)
stderr.Add(w.WithPriority(syslog.Err | syslog.Daemon))
}
}
return nil
}
// launch will launch the command defined in the session.
// This will return an error if the session fails to launch
func (t *tether) launch(session *SessionConfig) error {
defer trace.End(trace.Begin("launching session " + session.ID))
session.Lock()
defer session.Unlock()
var err error
defer func() {
if session.Started != "true" {
// if we didn't launch cleanly then clean up
t.cleanupSession(session)
}
// encode the result whether success or error
prefix := extraconfig.CalculateKeys(t.config, fmt.Sprintf("%s.%s", session.extraconfigKey, session.ID), "")[0]
log.Debugf("Encoding result of launch for session %s under key: %s", session.ID, prefix)
extraconfig.EncodeWithPrefix(t.sink, session, prefix)
}()
if len(session.User) > 0 || len(session.Group) > 0 {
user, err := getUserSysProcAttr(session.User, session.Group)
if err != nil {
log.Errorf("user lookup failed %s:%s, %s", session.User, session.Group, err)
session.Started = err.Error()
return err
}
session.Cmd.SysProcAttr = user
}
rootfs := ""
if session.ExecutionEnvironment != "" {
// session is supposed to run in the specific filesystem environment
// HACK: for now assume that the filesystem is mounted under /mnt/images/label, with label truncated to 15
label := session.ExecutionEnvironment
if len(label) > 15 {
label = label[:15]
}
rootfs = fmt.Sprintf("/mnt/images/%s/rootfs", label)
log.Infof("Configuring session to run in rootfs at %s", rootfs)
log.Debugf("Updating %+v for chroot", session.Cmd.SysProcAttr)
if useRunc {
prepareOCI(session, rootfs)
} else {
session.Cmd.SysProcAttr = chrootSysProcAttr(session.Cmd.SysProcAttr, rootfs)
}
}
if session.Diagnostics.ResurrectionCount > 0 {
// override session logging only while it's restarted to avoid break exec #6004
err = t.loggingLocked(session)
if err != nil {
log.Errorf("initializing logging for session failed with %s", err)
return err
}
}
session.Cmd.Env = t.ops.ProcessEnv(session.Cmd.Env)
// Set Std{in|out|err} to nil, we will control pipes
session.Cmd.Stdin = nil
session.Cmd.Stdout = nil
session.Cmd.Stderr = nil
// Set StopTime to its default value
session.StopTime = 0
resolved, err := lookPath(session.Cmd.Path, session.Cmd.Env, session.Cmd.Dir, rootfs)
if err != nil {
log.Errorf("Path lookup failed for %s: %s", session.Cmd.Path, err)
session.Started = err.Error()
return err
}
log.Debugf("Resolved %s to %s", session.Cmd.Path, resolved)
session.Cmd.Path = resolved
// block until we have a connection
if session.RunBlock && session.ClearToLaunch != nil {
log.Infof("Waiting clear signal to launch %s", session.ID)
select {
case <-t.ctx.Done():
log.Warnf("Waiting to launch %s canceled, bailing out", session.ID)
return nil
case <-session.ClearToLaunch:
log.Infof("Received the clear signal to launch %s", session.ID)
}
// reset RunBlock to unblock process start next time
session.RunBlock = false
}
pid := 0
// Use the mutex to make creating a child and adding the child pid into the
// childPidTable appear atomic to the reaper function. Use a anonymous function
// so we can defer unlocking locally
// logging is done after the function to keep the locked time as low as possible
err = func() error {
t.config.pidMutex.Lock()
defer t.config.pidMutex.Unlock()
if !session.Tty {
err = establishNonPty(session)
} else {
err = establishPty(session)
}
if err != nil {
return err
}
pid = session.Cmd.Process.Pid
t.config.pids[pid] = session
return nil
}()
if err != nil {
detail := fmt.Sprintf("failed to start container process: %s", err)
log.Error(detail)
// Set the Started key to the undecorated error message
session.Started = err.Error()
return errors.New(detail)
}
// ensure that this is updated so that we're correct for out-of-band power operations
// semantic should conform with port layer
session.StartTime = time.Now().UTC().Unix()
// Set the Started key to "true" - this indicates a successful launch
session.Started = "true"
// Write the PID to the associated PID file
cmdname := path.Base(session.Cmd.Path)
err = ioutil.WriteFile(fmt.Sprintf("%s.pid", path.Join(shared.PIDFileDir(), cmdname)),
[]byte(fmt.Sprintf("%d", pid)),
0644)
if err != nil {
log.Errorf("Unable to write PID file for %s: %s", cmdname, err)
}
log.Debugf("Launched command with pid %d", pid)
return nil
}
// prepareOCI creates a config.json for the image.
func prepareOCI(session *SessionConfig, rootfs string) error {
// Use runc to create a default spec
/* #nosec */
cmdRunc := exec.Command(runcPath, "spec")
cmdRunc.Dir = path.Dir(rootfs)
err := cmdRunc.Run()
if err != nil {
log.Errorf("Generating base OCI config file failed: %s", err.Error())
return err
}
// Load the spec
cfgFile := path.Join(cmdRunc.Dir, "config.json")
configBytes, err := ioutil.ReadFile(cfgFile)
if err != nil {
log.Errorf("Reading OCI config file failed: %s", err.Error())
return err
}
var configSpec specs.Spec
err = json.Unmarshal(configBytes, &configSpec)
if err != nil {
log.Errorf("Failed to unmarshal OCI config file: %s", err.Error())
return err
}
// Modify the spec
if configSpec.Root == nil {
configSpec.Root = &specs.Root{}
}
//TODO: Remove false default. This is here only for debugging purposes.
configSpec.Root.Readonly = false
if configSpec.Process == nil {
configSpec.Process = &specs.Process{}
}
configSpec.Process.Args = session.Cmd.Args
// Save the spec out to file
configBytes, err = json.Marshal(configSpec)
if err != nil {
log.Errorf("Failed to marshal OCI config file: %s", err.Error())
return err
}
err = ioutil.WriteFile(cfgFile, configBytes, 0644)
if err != nil {
log.Errorf("Failed to write out updated OCI config file: %s", err.Error())
return err
}
log.Infof("Updated OCI spec with process = %#v", *configSpec.Process)
// Finally, update the session to call runc
session.Cmd.Path = runcPath
session.Cmd.Args = append(session.Cmd.Args, "run")
session.Cmd.Args = append(session.Cmd.Args, "--no-pivot")
// Use session.ID as container name
session.Cmd.Args = append(session.Cmd.Args, session.ID)
session.Cmd.Dir = path.Dir(rootfs)
return nil
}
func logConfig(config *ExecutorConfig) {
// just pretty print the json for now
log.Info("Loaded executor config")
// figure out the keys to filter
keys := make(map[string]interface{})
if config.DebugLevel < 2 {
for _, f := range []string{
"Sessions.*.Cmd.Args",
"Sessions.*.Cmd.Args.*",
"Sessions.*.Cmd.Env",
"Sessions.*.Cmd.Env.*",
"Key"} {
for _, k := range extraconfig.CalculateKeys(config, f, "") {
keys[k] = nil
}
}
}
sink := map[string]string{}
extraconfig.Encode(
func(k, v string) error {
if _, ok := keys[k]; !ok {
sink[k] = v
}
return nil
},
config,
)
for k, v := range sink {
log.Debugf("%s: %s", k, v)
}
}
func (t *tether) forkHandler() {
defer trace.End(trace.Begin("start fork trigger handler"))
defer func() {
if r := recover(); r != nil {
log.Println("Recovered in forkHandler", r)
}
}()
incoming := make(chan os.Signal, 1)
signal.Notify(incoming, syscall.SIGABRT)
log.Info("SIGABRT handling initialized for fork support")
for range incoming {
// validate that this is a fork trigger and not just a random signal from
// container processes
log.Info("Received SIGABRT - preparing to transition to fork parent")
// TODO: record fork trigger in Config and persist
// TODO: do we need to rebind session executions stdio to /dev/null or to files?
err := t.ops.Fork()
if err != nil {
log.Errorf("vmfork failed:%s\n", err)
// TODO: how do we handle fork failure behaviour at a container level?
// Does it differ if triggered manually vs via pointcut conditions in a build file
continue
}
// trigger a reload of the configuration
log.Info("Triggering reload of config after fork")
t.reload <- struct{}{}
}
}
// restartableCmd takes the Cmd struct for a process that has been run and creates a new
// one that can be lauched again. Stdin/out will need to be set up again.
func restartableCmd(cmd *exec.Cmd) *exec.Cmd {
return &exec.Cmd{
Path: cmd.Path,
Args: cmd.Args,
Env: cmd.Env,
Dir: cmd.Dir,
ExtraFiles: cmd.ExtraFiles,
SysProcAttr: cmd.SysProcAttr,
}
}
// Config interface
func (t *tether) UpdateNetworkEndpoint(e *NetworkEndpoint) error {
defer trace.End(trace.Begin("tether.UpdateNetworkEndpoint"))
if e == nil {
return fmt.Errorf("endpoint must be specified")
}
if _, ok := t.config.Networks[e.Network.Name]; !ok {
return fmt.Errorf("network endpoint not found in config")
}
t.config.Networks[e.Network.Name] = e
return nil
}
func (t *tether) Flush() error {
defer trace.End(trace.Begin("tether.Flush"))
extraconfig.Encode(t.sink, t.config)
return nil
}
// killHelper was pulled from toolbox, and that variant should be directed at this
// one eventually
func killHelper(session *SessionConfig) error {
sig := new(msgs.SignalMsg)
name := session.StopSignal
if name == "" {
name = string(ssh.SIGTERM)
}
err := sig.FromString(name)
if err != nil {
return err
}
num := syscall.Signal(sig.Signum())
log.Infof("sending signal %s (%d) to %s", sig.Signal, num, session.ID)
if err := session.Cmd.Process.Signal(num); err != nil {
return fmt.Errorf("failed to signal %s: %s", session.ID, err)
}
return nil
}