Minimal, unintrusive implementation of a cleaner Job API.

* Implement a new package: engine. It exposes a useful but minimalist job API.
* Refactor main() to instanciate an Engine instead of a Server directly.
* Refactor server.go to register an engine job.

This is the smallest possible refactor which can include the new Engine design
into master. More gradual refactoring will follow.
Upstream-commit: 0d1a825137448e2f41e5aaa5ecae8094c8ab6817
Component: engine
This commit is contained in:
Solomon Hykes
2013-10-21 10:04:42 -06:00
parent af06b61d36
commit 2bbe323db0
6 changed files with 260 additions and 60 deletions

View File

@ -2,10 +2,14 @@ package docker
import (
"net"
"github.com/dotcloud/docker/engine"
)
// FIXME: separate runtime configuration from http api configuration
type DaemonConfig struct {
Pidfile string
// FIXME: don't call this GraphPath, it doesn't actually
// point to /var/lib/docker/graph, which is confusing.
GraphPath string
ProtoAddresses []string
AutoRestart bool
@ -16,3 +20,26 @@ type DaemonConfig struct {
DefaultIp net.IP
InterContainerCommunication bool
}
// ConfigGetenv creates and returns a new DaemonConfig object
// by parsing the contents of a job's environment.
func ConfigGetenv(job *engine.Job) *DaemonConfig {
var config DaemonConfig
config.Pidfile = job.Getenv("Pidfile")
config.GraphPath = job.Getenv("GraphPath")
config.AutoRestart = job.GetenvBool("AutoRestart")
config.EnableCors = job.GetenvBool("EnableCors")
if dns := job.Getenv("Dns"); dns != "" {
config.Dns = []string{dns}
}
config.EnableIptables = job.GetenvBool("EnableIptables")
if br := job.Getenv("BridgeIface"); br != "" {
config.BridgeIface = br
} else {
config.BridgeIface = DefaultNetworkBridge
}
config.ProtoAddresses = job.GetenvList("ProtoAddresses")
config.DefaultIp = net.ParseIP(job.Getenv("DefaultIp"))
config.InterContainerCommunication = job.GetenvBool("InterContainerCommunication")
return &config
}

View File

@ -6,9 +6,9 @@ import (
"github.com/dotcloud/docker"
"github.com/dotcloud/docker/sysinit"
"github.com/dotcloud/docker/utils"
"github.com/dotcloud/docker/engine"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"strconv"
@ -61,10 +61,6 @@ func main() {
}
}
bridge := docker.DefaultNetworkBridge
if *bridgeName != "" {
bridge = *bridgeName
}
if *flDebug {
os.Setenv("DEBUG", "1")
}
@ -75,26 +71,25 @@ func main() {
flag.Usage()
return
}
var dns []string
if *flDns != "" {
dns = []string{*flDns}
eng, err := engine.New(*flGraphPath)
if err != nil {
log.Fatal(err)
}
ip := net.ParseIP(*flDefaultIp)
config := &docker.DaemonConfig{
Pidfile: *pidfile,
GraphPath: *flGraphPath,
AutoRestart: *flAutoRestart,
EnableCors: *flEnableCors,
Dns: dns,
EnableIptables: *flEnableIptables,
BridgeIface: bridge,
ProtoAddresses: flHosts,
DefaultIp: ip,
InterContainerCommunication: *flInterContainerComm,
job, err := eng.Job("serveapi")
if err != nil {
log.Fatal(err)
}
if err := daemon(config); err != nil {
job.Setenv("Pidfile", *pidfile)
job.Setenv("GraphPath", *flGraphPath)
job.SetenvBool("AutoRestart", *flAutoRestart)
job.SetenvBool("EnableCors", *flEnableCors)
job.Setenv("Dns", *flDns)
job.SetenvBool("EnableIptables", *flEnableIptables)
job.Setenv("BridgeIface", *bridgeName)
job.SetenvList("ProtoAddresses", flHosts)
job.Setenv("DefaultIp", *flDefaultIp)
job.SetenvBool("InterContainerCommunication", *flInterContainerComm)
if err := daemon(job, *pidfile); err != nil {
log.Fatal(err)
}
} else {
@ -142,51 +137,22 @@ func removePidFile(pidfile string) {
}
}
func daemon(config *docker.DaemonConfig) error {
if err := createPidFile(config.Pidfile); err != nil {
// daemon runs `job` as a daemon.
// A pidfile is created for the duration of the job,
// and all signals are intercepted.
func daemon(job *engine.Job, pidfile string) error {
if err := createPidFile(pidfile); err != nil {
log.Fatal(err)
}
defer removePidFile(config.Pidfile)
server, err := docker.NewServer(config)
if err != nil {
return err
}
defer server.Close()
defer removePidFile(pidfile)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill, os.Signal(syscall.SIGTERM))
go func() {
sig := <-c
log.Printf("Received signal '%v', exiting\n", sig)
server.Close()
removePidFile(config.Pidfile)
removePidFile(pidfile)
os.Exit(0)
}()
chErrors := make(chan error, len(config.ProtoAddresses))
for _, protoAddr := range config.ProtoAddresses {
protoAddrParts := strings.SplitN(protoAddr, "://", 2)
if protoAddrParts[0] == "unix" {
syscall.Unlink(protoAddrParts[1])
} else if protoAddrParts[0] == "tcp" {
if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") {
log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
}
} else {
server.Close()
removePidFile(config.Pidfile)
log.Fatal("Invalid protocol format.")
}
go func() {
chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true)
}()
}
for i := 0; i < len(config.ProtoAddresses); i += 1 {
err := <-chErrors
if err != nil {
return err
}
}
return nil
return job.Run()
}

View File

@ -0,0 +1 @@
Solomon Hykes <solomon@dotcloud.com>

View File

@ -0,0 +1,62 @@
package engine
import (
"fmt"
"os"
)
type Handler func(*Job) string
var globalHandlers map[string]Handler
func Register(name string, handler Handler) error {
if globalHandlers == nil {
globalHandlers = make(map[string]Handler)
}
globalHandlers[name] = handler
return nil
}
// The Engine is the core of Docker.
// It acts as a store for *containers*, and allows manipulation of these
// containers by executing *jobs*.
type Engine struct {
root string
handlers map[string]Handler
}
// New initializes a new engine managing the directory specified at `root`.
// `root` is used to store containers and any other state private to the engine.
// Changing the contents of the root without executing a job will cause unspecified
// behavior.
func New(root string) (*Engine, error) {
if err := os.MkdirAll(root, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
eng := &Engine{
root: root,
handlers: globalHandlers,
}
return eng, nil
}
// Job creates a new job which can later be executed.
// This function mimics `Command` from the standard os/exec package.
func (eng *Engine) Job(name string, args ...string) (*Job, error) {
handler, exists := eng.handlers[name]
if !exists || handler == nil {
return nil, fmt.Errorf("Undefined command; %s", name)
}
job := &Job{
eng: eng,
Name: name,
Args: args,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
handler: handler,
}
return job, nil
}

View File

@ -0,0 +1,105 @@
package engine
import (
"io"
"strings"
"fmt"
"encoding/json"
)
// A job is the fundamental unit of work in the docker engine.
// Everything docker can do should eventually be exposed as a job.
// For example: execute a process in a container, create a new container,
// download an archive from the internet, serve the http api, etc.
//
// The job API is designed after unix processes: a job has a name, arguments,
// environment variables, standard streams for input, output and error, and
// an exit status which can indicate success (0) or error (anything else).
//
// One slight variation is that jobs report their status as a string. The
// string "0" indicates success, and any other strings indicates an error.
// This allows for richer error reporting.
//
type Job struct {
eng *Engine
Name string
Args []string
env []string
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
handler func(*Job) string
status string
}
// Run executes the job and blocks until the job completes.
// If the job returns a failure status, an error is returned
// which includes the status.
func (job *Job) Run() error {
if job.handler == nil {
return fmt.Errorf("Undefined job handler")
}
status := job.handler(job)
job.status = status
if status != "0" {
return fmt.Errorf("Job failed with status %s", status)
}
return nil
}
func (job *Job) Getenv(key string) (value string) {
for _, kv := range job.env {
if strings.Index(kv, "=") == -1 {
continue
}
parts := strings.SplitN(kv, "=", 2)
if parts[0] != key {
continue
}
if len(parts) < 2 {
value = ""
} else {
value = parts[1]
}
}
return
}
func (job *Job) GetenvBool(key string) (value bool) {
s := strings.ToLower(strings.Trim(job.Getenv(key), " \t"))
if s == "" || s == "0" || s == "no" || s == "false" || s == "none" {
return false
}
return true
}
func (job *Job) SetenvBool(key string, value bool) {
if value {
job.Setenv(key, "1")
} else {
job.Setenv(key, "0")
}
}
func (job *Job) GetenvList(key string) []string {
sval := job.Getenv(key)
l := make([]string, 0, 1)
if err := json.Unmarshal([]byte(sval), &l); err != nil {
l = append(l, sval)
}
return l
}
func (job *Job) SetenvList(key string, value []string) error {
sval, err := json.Marshal(value)
if err != nil {
return err
}
job.Setenv(key, string(sval))
return nil
}
func (job *Job) Setenv(key, value string) {
job.env = append(job.env, key + "=" + value)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/dotcloud/docker/gograph"
"github.com/dotcloud/docker/registry"
"github.com/dotcloud/docker/utils"
"github.com/dotcloud/docker/engine"
"io"
"io/ioutil"
"log"
@ -22,12 +23,50 @@ import (
"strings"
"sync"
"time"
"syscall"
)
func (srv *Server) Close() error {
return srv.runtime.Close()
}
func init() {
engine.Register("serveapi", JobServeApi)
}
func JobServeApi(job *engine.Job) string {
srv, err := NewServer(ConfigGetenv(job))
if err != nil {
return err.Error()
}
defer srv.Close()
// Parse addresses to serve on
protoAddrs := job.Args
chErrors := make(chan error, len(protoAddrs))
for _, protoAddr := range protoAddrs {
protoAddrParts := strings.SplitN(protoAddr, "://", 2)
if protoAddrParts[0] == "unix" {
syscall.Unlink(protoAddrParts[1])
} else if protoAddrParts[0] == "tcp" {
if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") {
log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
}
} else {
return "Invalid protocol format."
}
go func() {
chErrors <- ListenAndServe(protoAddrParts[0], protoAddrParts[1], srv, true)
}()
}
for i := 0; i < len(protoAddrs); i += 1 {
err := <-chErrors
if err != nil {
return err.Error()
}
}
return "0"
}
func (srv *Server) DockerVersion() APIVersion {
return APIVersion{
Version: VERSION,