diff --git a/config.go b/config.go index 8f2d22c255..a572cfadaf 100644 --- a/config.go +++ b/config.go @@ -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 +} diff --git a/docker/docker.go b/docker/docker.go index 362e899ba1..7487dba86f 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -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() } diff --git a/engine/MAINTAINERS b/engine/MAINTAINERS new file mode 100644 index 0000000000..e1c6f2ccfc --- /dev/null +++ b/engine/MAINTAINERS @@ -0,0 +1 @@ +Solomon Hykes diff --git a/engine/engine.go b/engine/engine.go new file mode 100644 index 0000000000..e253678069 --- /dev/null +++ b/engine/engine.go @@ -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 +} + diff --git a/engine/job.go b/engine/job.go new file mode 100644 index 0000000000..8ff722d2fb --- /dev/null +++ b/engine/job.go @@ -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) +} diff --git a/server.go b/server.go index d1bbe1bfe3..d833f06b7f 100644 --- a/server.go +++ b/server.go @@ -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,