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.
This commit is contained in:
Solomon Hykes 2013-10-21 10:04:42 -06:00
parent 60b97576cf
commit 0d1a825137
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()
}

1
engine/MAINTAINERS Normal file
View File

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

62
engine/engine.go Normal file
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
}

105
engine/job.go Normal file
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,