* 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.
... | ... |
@@ -2,10 +2,14 @@ package docker |
2 | 2 |
|
3 | 3 |
import ( |
4 | 4 |
"net" |
5 |
+ "github.com/dotcloud/docker/engine" |
|
5 | 6 |
) |
6 | 7 |
|
8 |
+// FIXME: separate runtime configuration from http api configuration |
|
7 | 9 |
type DaemonConfig struct { |
8 | 10 |
Pidfile string |
11 |
+ // FIXME: don't call this GraphPath, it doesn't actually |
|
12 |
+ // point to /var/lib/docker/graph, which is confusing. |
|
9 | 13 |
GraphPath string |
10 | 14 |
ProtoAddresses []string |
11 | 15 |
AutoRestart bool |
... | ... |
@@ -16,3 +20,26 @@ type DaemonConfig struct { |
16 | 16 |
DefaultIp net.IP |
17 | 17 |
InterContainerCommunication bool |
18 | 18 |
} |
19 |
+ |
|
20 |
+// ConfigGetenv creates and returns a new DaemonConfig object |
|
21 |
+// by parsing the contents of a job's environment. |
|
22 |
+func ConfigGetenv(job *engine.Job) *DaemonConfig { |
|
23 |
+ var config DaemonConfig |
|
24 |
+ config.Pidfile = job.Getenv("Pidfile") |
|
25 |
+ config.GraphPath = job.Getenv("GraphPath") |
|
26 |
+ config.AutoRestart = job.GetenvBool("AutoRestart") |
|
27 |
+ config.EnableCors = job.GetenvBool("EnableCors") |
|
28 |
+ if dns := job.Getenv("Dns"); dns != "" { |
|
29 |
+ config.Dns = []string{dns} |
|
30 |
+ } |
|
31 |
+ config.EnableIptables = job.GetenvBool("EnableIptables") |
|
32 |
+ if br := job.Getenv("BridgeIface"); br != "" { |
|
33 |
+ config.BridgeIface = br |
|
34 |
+ } else { |
|
35 |
+ config.BridgeIface = DefaultNetworkBridge |
|
36 |
+ } |
|
37 |
+ config.ProtoAddresses = job.GetenvList("ProtoAddresses") |
|
38 |
+ config.DefaultIp = net.ParseIP(job.Getenv("DefaultIp")) |
|
39 |
+ config.InterContainerCommunication = job.GetenvBool("InterContainerCommunication") |
|
40 |
+ return &config |
|
41 |
+} |
... | ... |
@@ -6,9 +6,9 @@ import ( |
6 | 6 |
"github.com/dotcloud/docker" |
7 | 7 |
"github.com/dotcloud/docker/sysinit" |
8 | 8 |
"github.com/dotcloud/docker/utils" |
9 |
+ "github.com/dotcloud/docker/engine" |
|
9 | 10 |
"io/ioutil" |
10 | 11 |
"log" |
11 |
- "net" |
|
12 | 12 |
"os" |
13 | 13 |
"os/signal" |
14 | 14 |
"strconv" |
... | ... |
@@ -61,10 +61,6 @@ func main() { |
61 | 61 |
} |
62 | 62 |
} |
63 | 63 |
|
64 |
- bridge := docker.DefaultNetworkBridge |
|
65 |
- if *bridgeName != "" { |
|
66 |
- bridge = *bridgeName |
|
67 |
- } |
|
68 | 64 |
if *flDebug { |
69 | 65 |
os.Setenv("DEBUG", "1") |
70 | 66 |
} |
... | ... |
@@ -75,26 +71,25 @@ func main() { |
75 | 75 |
flag.Usage() |
76 | 76 |
return |
77 | 77 |
} |
78 |
- var dns []string |
|
79 |
- if *flDns != "" { |
|
80 |
- dns = []string{*flDns} |
|
78 |
+ eng, err := engine.New(*flGraphPath) |
|
79 |
+ if err != nil { |
|
80 |
+ log.Fatal(err) |
|
81 | 81 |
} |
82 |
- |
|
83 |
- ip := net.ParseIP(*flDefaultIp) |
|
84 |
- |
|
85 |
- config := &docker.DaemonConfig{ |
|
86 |
- Pidfile: *pidfile, |
|
87 |
- GraphPath: *flGraphPath, |
|
88 |
- AutoRestart: *flAutoRestart, |
|
89 |
- EnableCors: *flEnableCors, |
|
90 |
- Dns: dns, |
|
91 |
- EnableIptables: *flEnableIptables, |
|
92 |
- BridgeIface: bridge, |
|
93 |
- ProtoAddresses: flHosts, |
|
94 |
- DefaultIp: ip, |
|
95 |
- InterContainerCommunication: *flInterContainerComm, |
|
82 |
+ job, err := eng.Job("serveapi") |
|
83 |
+ if err != nil { |
|
84 |
+ log.Fatal(err) |
|
96 | 85 |
} |
97 |
- if err := daemon(config); err != nil { |
|
86 |
+ job.Setenv("Pidfile", *pidfile) |
|
87 |
+ job.Setenv("GraphPath", *flGraphPath) |
|
88 |
+ job.SetenvBool("AutoRestart", *flAutoRestart) |
|
89 |
+ job.SetenvBool("EnableCors", *flEnableCors) |
|
90 |
+ job.Setenv("Dns", *flDns) |
|
91 |
+ job.SetenvBool("EnableIptables", *flEnableIptables) |
|
92 |
+ job.Setenv("BridgeIface", *bridgeName) |
|
93 |
+ job.SetenvList("ProtoAddresses", flHosts) |
|
94 |
+ job.Setenv("DefaultIp", *flDefaultIp) |
|
95 |
+ job.SetenvBool("InterContainerCommunication", *flInterContainerComm) |
|
96 |
+ if err := daemon(job, *pidfile); err != nil { |
|
98 | 97 |
log.Fatal(err) |
99 | 98 |
} |
100 | 99 |
} else { |
... | ... |
@@ -142,51 +137,22 @@ func removePidFile(pidfile string) { |
142 | 142 |
} |
143 | 143 |
} |
144 | 144 |
|
145 |
-func daemon(config *docker.DaemonConfig) error { |
|
146 |
- if err := createPidFile(config.Pidfile); err != nil { |
|
145 |
+// daemon runs `job` as a daemon. |
|
146 |
+// A pidfile is created for the duration of the job, |
|
147 |
+// and all signals are intercepted. |
|
148 |
+func daemon(job *engine.Job, pidfile string) error { |
|
149 |
+ if err := createPidFile(pidfile); err != nil { |
|
147 | 150 |
log.Fatal(err) |
148 | 151 |
} |
149 |
- defer removePidFile(config.Pidfile) |
|
150 |
- |
|
151 |
- server, err := docker.NewServer(config) |
|
152 |
- if err != nil { |
|
153 |
- return err |
|
154 |
- } |
|
155 |
- defer server.Close() |
|
152 |
+ defer removePidFile(pidfile) |
|
156 | 153 |
|
157 | 154 |
c := make(chan os.Signal, 1) |
158 | 155 |
signal.Notify(c, os.Interrupt, os.Kill, os.Signal(syscall.SIGTERM)) |
159 | 156 |
go func() { |
160 | 157 |
sig := <-c |
161 | 158 |
log.Printf("Received signal '%v', exiting\n", sig) |
162 |
- server.Close() |
|
163 |
- removePidFile(config.Pidfile) |
|
159 |
+ removePidFile(pidfile) |
|
164 | 160 |
os.Exit(0) |
165 | 161 |
}() |
166 |
- |
|
167 |
- chErrors := make(chan error, len(config.ProtoAddresses)) |
|
168 |
- for _, protoAddr := range config.ProtoAddresses { |
|
169 |
- protoAddrParts := strings.SplitN(protoAddr, "://", 2) |
|
170 |
- if protoAddrParts[0] == "unix" { |
|
171 |
- syscall.Unlink(protoAddrParts[1]) |
|
172 |
- } else if protoAddrParts[0] == "tcp" { |
|
173 |
- if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") { |
|
174 |
- log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") |
|
175 |
- } |
|
176 |
- } else { |
|
177 |
- server.Close() |
|
178 |
- removePidFile(config.Pidfile) |
|
179 |
- log.Fatal("Invalid protocol format.") |
|
180 |
- } |
|
181 |
- go func() { |
|
182 |
- chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true) |
|
183 |
- }() |
|
184 |
- } |
|
185 |
- for i := 0; i < len(config.ProtoAddresses); i += 1 { |
|
186 |
- err := <-chErrors |
|
187 |
- if err != nil { |
|
188 |
- return err |
|
189 |
- } |
|
190 |
- } |
|
191 |
- return nil |
|
162 |
+ return job.Run() |
|
192 | 163 |
} |
0 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,62 @@ |
0 |
+package engine |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "os" |
|
5 |
+) |
|
6 |
+ |
|
7 |
+ |
|
8 |
+type Handler func(*Job) string |
|
9 |
+ |
|
10 |
+var globalHandlers map[string]Handler |
|
11 |
+ |
|
12 |
+func Register(name string, handler Handler) error { |
|
13 |
+ if globalHandlers == nil { |
|
14 |
+ globalHandlers = make(map[string]Handler) |
|
15 |
+ } |
|
16 |
+ globalHandlers[name] = handler |
|
17 |
+ return nil |
|
18 |
+} |
|
19 |
+ |
|
20 |
+// The Engine is the core of Docker. |
|
21 |
+// It acts as a store for *containers*, and allows manipulation of these |
|
22 |
+// containers by executing *jobs*. |
|
23 |
+type Engine struct { |
|
24 |
+ root string |
|
25 |
+ handlers map[string]Handler |
|
26 |
+} |
|
27 |
+ |
|
28 |
+// New initializes a new engine managing the directory specified at `root`. |
|
29 |
+// `root` is used to store containers and any other state private to the engine. |
|
30 |
+// Changing the contents of the root without executing a job will cause unspecified |
|
31 |
+// behavior. |
|
32 |
+func New(root string) (*Engine, error) { |
|
33 |
+ if err := os.MkdirAll(root, 0700); err != nil && !os.IsExist(err) { |
|
34 |
+ return nil, err |
|
35 |
+ } |
|
36 |
+ eng := &Engine{ |
|
37 |
+ root: root, |
|
38 |
+ handlers: globalHandlers, |
|
39 |
+ } |
|
40 |
+ return eng, nil |
|
41 |
+} |
|
42 |
+ |
|
43 |
+// Job creates a new job which can later be executed. |
|
44 |
+// This function mimics `Command` from the standard os/exec package. |
|
45 |
+func (eng *Engine) Job(name string, args ...string) (*Job, error) { |
|
46 |
+ handler, exists := eng.handlers[name] |
|
47 |
+ if !exists || handler == nil { |
|
48 |
+ return nil, fmt.Errorf("Undefined command; %s", name) |
|
49 |
+ } |
|
50 |
+ job := &Job{ |
|
51 |
+ eng: eng, |
|
52 |
+ Name: name, |
|
53 |
+ Args: args, |
|
54 |
+ Stdin: os.Stdin, |
|
55 |
+ Stdout: os.Stdout, |
|
56 |
+ Stderr: os.Stderr, |
|
57 |
+ handler: handler, |
|
58 |
+ } |
|
59 |
+ return job, nil |
|
60 |
+} |
|
61 |
+ |
0 | 62 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,105 @@ |
0 |
+package engine |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "io" |
|
4 |
+ "strings" |
|
5 |
+ "fmt" |
|
6 |
+ "encoding/json" |
|
7 |
+) |
|
8 |
+ |
|
9 |
+// A job is the fundamental unit of work in the docker engine. |
|
10 |
+// Everything docker can do should eventually be exposed as a job. |
|
11 |
+// For example: execute a process in a container, create a new container, |
|
12 |
+// download an archive from the internet, serve the http api, etc. |
|
13 |
+// |
|
14 |
+// The job API is designed after unix processes: a job has a name, arguments, |
|
15 |
+// environment variables, standard streams for input, output and error, and |
|
16 |
+// an exit status which can indicate success (0) or error (anything else). |
|
17 |
+// |
|
18 |
+// One slight variation is that jobs report their status as a string. The |
|
19 |
+// string "0" indicates success, and any other strings indicates an error. |
|
20 |
+// This allows for richer error reporting. |
|
21 |
+// |
|
22 |
+type Job struct { |
|
23 |
+ eng *Engine |
|
24 |
+ Name string |
|
25 |
+ Args []string |
|
26 |
+ env []string |
|
27 |
+ Stdin io.ReadCloser |
|
28 |
+ Stdout io.WriteCloser |
|
29 |
+ Stderr io.WriteCloser |
|
30 |
+ handler func(*Job) string |
|
31 |
+ status string |
|
32 |
+} |
|
33 |
+ |
|
34 |
+// Run executes the job and blocks until the job completes. |
|
35 |
+// If the job returns a failure status, an error is returned |
|
36 |
+// which includes the status. |
|
37 |
+func (job *Job) Run() error { |
|
38 |
+ if job.handler == nil { |
|
39 |
+ return fmt.Errorf("Undefined job handler") |
|
40 |
+ } |
|
41 |
+ status := job.handler(job) |
|
42 |
+ job.status = status |
|
43 |
+ if status != "0" { |
|
44 |
+ return fmt.Errorf("Job failed with status %s", status) |
|
45 |
+ } |
|
46 |
+ return nil |
|
47 |
+} |
|
48 |
+ |
|
49 |
+ |
|
50 |
+func (job *Job) Getenv(key string) (value string) { |
|
51 |
+ for _, kv := range job.env { |
|
52 |
+ if strings.Index(kv, "=") == -1 { |
|
53 |
+ continue |
|
54 |
+ } |
|
55 |
+ parts := strings.SplitN(kv, "=", 2) |
|
56 |
+ if parts[0] != key { |
|
57 |
+ continue |
|
58 |
+ } |
|
59 |
+ if len(parts) < 2 { |
|
60 |
+ value = "" |
|
61 |
+ } else { |
|
62 |
+ value = parts[1] |
|
63 |
+ } |
|
64 |
+ } |
|
65 |
+ return |
|
66 |
+} |
|
67 |
+ |
|
68 |
+func (job *Job) GetenvBool(key string) (value bool) { |
|
69 |
+ s := strings.ToLower(strings.Trim(job.Getenv(key), " \t")) |
|
70 |
+ if s == "" || s == "0" || s == "no" || s == "false" || s == "none" { |
|
71 |
+ return false |
|
72 |
+ } |
|
73 |
+ return true |
|
74 |
+} |
|
75 |
+ |
|
76 |
+func (job *Job) SetenvBool(key string, value bool) { |
|
77 |
+ if value { |
|
78 |
+ job.Setenv(key, "1") |
|
79 |
+ } else { |
|
80 |
+ job.Setenv(key, "0") |
|
81 |
+ } |
|
82 |
+} |
|
83 |
+ |
|
84 |
+func (job *Job) GetenvList(key string) []string { |
|
85 |
+ sval := job.Getenv(key) |
|
86 |
+ l := make([]string, 0, 1) |
|
87 |
+ if err := json.Unmarshal([]byte(sval), &l); err != nil { |
|
88 |
+ l = append(l, sval) |
|
89 |
+ } |
|
90 |
+ return l |
|
91 |
+} |
|
92 |
+ |
|
93 |
+func (job *Job) SetenvList(key string, value []string) error { |
|
94 |
+ sval, err := json.Marshal(value) |
|
95 |
+ if err != nil { |
|
96 |
+ return err |
|
97 |
+ } |
|
98 |
+ job.Setenv(key, string(sval)) |
|
99 |
+ return nil |
|
100 |
+} |
|
101 |
+ |
|
102 |
+func (job *Job) Setenv(key, value string) { |
|
103 |
+ job.env = append(job.env, key + "=" + value) |
|
104 |
+} |
... | ... |
@@ -9,6 +9,7 @@ import ( |
9 | 9 |
"github.com/dotcloud/docker/gograph" |
10 | 10 |
"github.com/dotcloud/docker/registry" |
11 | 11 |
"github.com/dotcloud/docker/utils" |
12 |
+ "github.com/dotcloud/docker/engine" |
|
12 | 13 |
"io" |
13 | 14 |
"io/ioutil" |
14 | 15 |
"log" |
... | ... |
@@ -22,12 +23,50 @@ import ( |
22 | 22 |
"strings" |
23 | 23 |
"sync" |
24 | 24 |
"time" |
25 |
+ "syscall" |
|
25 | 26 |
) |
26 | 27 |
|
27 | 28 |
func (srv *Server) Close() error { |
28 | 29 |
return srv.runtime.Close() |
29 | 30 |
} |
30 | 31 |
|
32 |
+func init() { |
|
33 |
+ engine.Register("serveapi", JobServeApi) |
|
34 |
+} |
|
35 |
+ |
|
36 |
+func JobServeApi(job *engine.Job) string { |
|
37 |
+ srv, err := NewServer(ConfigGetenv(job)) |
|
38 |
+ if err != nil { |
|
39 |
+ return err.Error() |
|
40 |
+ } |
|
41 |
+ defer srv.Close() |
|
42 |
+ // Parse addresses to serve on |
|
43 |
+ protoAddrs := job.Args |
|
44 |
+ chErrors := make(chan error, len(protoAddrs)) |
|
45 |
+ for _, protoAddr := range protoAddrs { |
|
46 |
+ protoAddrParts := strings.SplitN(protoAddr, "://", 2) |
|
47 |
+ if protoAddrParts[0] == "unix" { |
|
48 |
+ syscall.Unlink(protoAddrParts[1]) |
|
49 |
+ } else if protoAddrParts[0] == "tcp" { |
|
50 |
+ if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") { |
|
51 |
+ log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") |
|
52 |
+ } |
|
53 |
+ } else { |
|
54 |
+ return "Invalid protocol format." |
|
55 |
+ } |
|
56 |
+ go func() { |
|
57 |
+ chErrors <- ListenAndServe(protoAddrParts[0], protoAddrParts[1], srv, true) |
|
58 |
+ }() |
|
59 |
+ } |
|
60 |
+ for i := 0; i < len(protoAddrs); i += 1 { |
|
61 |
+ err := <-chErrors |
|
62 |
+ if err != nil { |
|
63 |
+ return err.Error() |
|
64 |
+ } |
|
65 |
+ } |
|
66 |
+ return "0" |
|
67 |
+} |
|
68 |
+ |
|
31 | 69 |
func (srv *Server) DockerVersion() APIVersion { |
32 | 70 |
return APIVersion{ |
33 | 71 |
Version: VERSION, |