open-nomad/client/exec/exec.go

148 lines
4.4 KiB
Go
Raw Normal View History

// Package exec is used to invoke child processes across various platforms to
// provide the following features:
//
// - Least privilege
// - Resource constraints
// - Process isolation
//
// A "platform" may be defined as coarsely as "Windows" or as specifically as
// "linux 3.20 with systemd". This allows Nomad to use best-effort, best-
// available capabilities of each platform to provide resource constraints,
// process isolation, and security features, or otherwise take advantage of
// features that are unique to that platform.
//
// The semantics of any particular instance are left up to the implementation.
// However, these should be completely transparent to the calling context. In
// other words, the Java driver should be able to call exec for any platform and
// just work.
package exec
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/hashicorp/nomad/nomad/structs"
)
// Executor is an interface that any platform- or capability-specific exec
// wrapper must implement. You should not need to implement a Java executor.
// Rather, you would implement a cgroups executor that the Java driver will use.
type Executor interface {
// Limit must be called before Start and restricts the amount of resources
// the process can use
Limit(structs.Resources)
// RunAs sets the user we should use to run this command. This may be set as
// a username, uid, or other identifier. The implementation will decide what
// to do with it, if anything.
RunAs(string)
// Start the process. This may wrap the actual process in another command,
// depending on the capabilities in this environment. Errors that arise from
// Limits or Runas will bubble through Start()
Start() error
// Open should be called to restore a previous pid. This might be needed if
// nomad is restarted. This sets os.Process internally.
Open(int) error
// Shutdown should use a graceful stop mechanism so the application can
// perform checkpointing or cleanup, if such a mechanism is available.
// If such a mechanism is not available, Shutdown() should call ForceStop().
Shutdown() error
// ForceStop will terminate the process without waiting for cleanup. Every
// implementations must provide this.
ForceStop() error
// Access the underlying Cmd struct. This should never be nil. Also, this is
// not intended to be access outside the exec package, so YMMV.
Command() *cmd
}
// Cmd is an extension of exec.Cmd that incorporates functionality for
// re-attaching to processes, dropping priviledges, etc., based on platform-
// specific implementations.
type cmd struct {
exec.Cmd
// Resources is used to limit CPU and RAM used by the process, by way of
// cgroups or a similar mechanism.
Resources structs.Resources
// RunAs may be a username or Uid. The implementation will decide how to use it.
RunAs string
}
// Command is a mirror of exec.Command that returns a platform-specific Executor
func Command(name string, arg ...string) Executor {
executor := AutoselectExecutor()
cmd := executor.Command()
cmd.Path = name
cmd.Args = append([]string{name}, arg...)
if filepath.Base(name) == name {
if lp, err := exec.LookPath(name); err != nil {
// cmd.lookPathErr = err
} else {
cmd.Path = lp
}
}
return executor
}
func OpenPid(int) Executor {
executor := AutoselectExecutor()
return executor
}
// AutoselectExecutor uses capability testing to give you the best available
// executor based on your platform and execution environment. If you need a
// specific executor, call it directly.
func AutoselectExecutor() Executor {
// TODO platform switching
return &UniversalExecutor{}
}
// UniversalExecutor should work everywhere, and as a result does not include
// any resource restrictions or runas capabilities.
type UniversalExecutor struct {
cmd
}
func (e *UniversalExecutor) Limit(resources structs.Resources) {
// No-op
}
func (e *UniversalExecutor) RunAs(userid string) {
// No-op
}
func (e *UniversalExecutor) Start() error {
// We don't want to call ourself. We want to call Start on our embedded Cmd
return e.cmd.Start()
}
func (e *UniversalExecutor) Open(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("Failed to reopen pid %d: %s", pid, err)
}
e.Process = process
return nil
}
func (e *UniversalExecutor) Shutdown() error {
return e.ForceStop()
}
func (e *UniversalExecutor) ForceStop() error {
return e.Process.Kill()
}
func (e *UniversalExecutor) Command() *cmd {
return &e.cmd
}