Browse code

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.

Solomon Hykes authored on 2013/10/22 01:04:42
Showing 6 changed files
... ...
@@ -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
 }
193 164
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+Solomon Hykes <solomon@dotcloud.com>
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,