diff --git a/api.go b/api.go index 0c06b2e6d..1a3d92de3 100644 --- a/api.go +++ b/api.go @@ -639,26 +639,20 @@ func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.R } func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - var hostConfig *HostConfig - // allow a nil body for backwards compatibility - if r.Body != nil { - if matchesContentType(r.Header.Get("Content-Type"), "application/json") { - hostConfig = &HostConfig{} - if err := json.NewDecoder(r.Body).Decode(hostConfig); err != nil { - return err - } - } - } - if vars == nil { return fmt.Errorf("Missing parameter") } name := vars["name"] - // Register any links from the host config before starting the container - if err := srv.RegisterLinks(name, hostConfig); err != nil { - return err + job := srv.Eng.Job("start", name) + // allow a nil body for backwards compatibility + if r.Body != nil { + if matchesContentType(r.Header.Get("Content-Type"), "application/json") { + if err := job.DecodeEnv(r.Body); err != nil { + return err + } + } } - if err := srv.ContainerStart(name, hostConfig); err != nil { + if err := job.Run(); err != nil { return err } w.WriteHeader(http.StatusNoContent) diff --git a/api_test.go b/api_test.go index fcca8ce83..50a86e3f2 100644 --- a/api_test.go +++ b/api_test.go @@ -781,11 +781,11 @@ func TestPostContainersRestart(t *testing.T) { } func TestPostContainersStart(t *testing.T) { - runtime := mkRuntime(t) + eng := NewTestEngine(t) + srv := mkServerFromEngine(eng, t) + runtime := srv.runtime defer nuke(runtime) - srv := &Server{runtime: runtime} - container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, diff --git a/engine/engine.go b/engine/engine.go index 8d67242ca..a0d5a3c4a 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -13,10 +13,11 @@ type Handler func(*Job) string var globalHandlers map[string]Handler +func init() { + globalHandlers = make(map[string]Handler) +} + func Register(name string, handler Handler) error { - if globalHandlers == nil { - globalHandlers = make(map[string]Handler) - } globalHandlers[name] = handler return nil } @@ -27,6 +28,7 @@ func Register(name string, handler Handler) error { type Engine struct { root string handlers map[string]Handler + hack Hack // data for temporary hackery (see hack.go) } // New initializes a new engine managing the directory specified at `root`. @@ -66,7 +68,7 @@ func New(root string) (*Engine, error) { // This function mimics `Command` from the standard os/exec package. func (eng *Engine) Job(name string, args ...string) *Job { job := &Job{ - eng: eng, + Eng: eng, Name: name, Args: args, Stdin: os.Stdin, diff --git a/engine/hack.go b/engine/hack.go new file mode 100644 index 000000000..7f6e79c0e --- /dev/null +++ b/engine/hack.go @@ -0,0 +1,23 @@ +package engine + + +type Hack map[string]interface{} + + +func (eng *Engine) Hack_GetGlobalVar(key string) interface{} { + if eng.hack == nil { + return nil + } + val, exists := eng.hack[key] + if !exists { + return nil + } + return val +} + +func (eng *Engine) Hack_SetGlobalVar(key string, val interface{}) { + if eng.hack == nil { + eng.hack = make(Hack) + } + eng.hack[key] = val +} diff --git a/engine/job.go b/engine/job.go index 0bde2a0be..5c02fe15d 100644 --- a/engine/job.go +++ b/engine/job.go @@ -1,6 +1,7 @@ package engine import ( + "bytes" "io" "strings" "fmt" @@ -22,7 +23,7 @@ import ( // This allows for richer error reporting. // type Job struct { - eng *Engine + Eng *Engine Name string Args []string env []string @@ -111,3 +112,60 @@ func (job *Job) SetenvList(key string, value []string) error { func (job *Job) Setenv(key, value string) { job.env = append(job.env, key + "=" + value) } + +// DecodeEnv decodes `src` as a json dictionary, and adds +// each decoded key-value pair to the environment. +// +// If `text` cannot be decoded as a json dictionary, an error +// is returned. +func (job *Job) DecodeEnv(src io.Reader) error { + m := make(map[string]interface{}) + if err := json.NewDecoder(src).Decode(&m); err != nil { + return err + } + for k, v := range m { + if sval, ok := v.(string); ok { + job.Setenv(k, sval) + } else if val, err := json.Marshal(v); err == nil { + job.Setenv(k, string(val)) + } else { + job.Setenv(k, fmt.Sprintf("%v", v)) + } + } + return nil +} + +func (job *Job) EncodeEnv(dst io.Writer) error { + return json.NewEncoder(dst).Encode(job.Environ()) +} + +func (job *Job) ExportEnv(dst interface{}) error { + var buf bytes.Buffer + if err := job.EncodeEnv(&buf); err != nil { + return err + } + if err := json.NewDecoder(&buf).Decode(dst); err != nil { + return err + } + return nil +} + +func (job *Job) ImportEnv(src interface{}) error { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(src); err != nil { + return err + } + if err := job.DecodeEnv(&buf); err != nil { + return err + } + return nil +} + +func (job *Job) Environ() map[string]string { + m := make(map[string]string) + for _, kv := range job.env { + parts := strings.SplitN(kv, "=", 2) + m[parts[0]] = parts[1] + } + return m +} diff --git a/engine/init_test.go b/engine/utils.go similarity index 90% rename from engine/init_test.go rename to engine/utils.go index 5c03ded87..58e5f9aae 100644 --- a/engine/init_test.go +++ b/engine/utils.go @@ -15,7 +15,7 @@ func init() { Register("dummy", func(job *Job) string { return ""; }) } -func mkEngine(t *testing.T) *Engine { +func NewTestEngine(t *testing.T) *Engine { // Use the caller function name as a prefix. // This helps trace temp directories back to their test. pc, _, _, _ := runtime.Caller(1) @@ -38,5 +38,5 @@ func mkEngine(t *testing.T) *Engine { } func mkJob(t *testing.T, name string, args ...string) *Job { - return mkEngine(t).Job(name, args...) + return NewTestEngine(t).Job(name, args...) } diff --git a/server.go b/server.go index d8b70b88a..efc34d14e 100644 --- a/server.go +++ b/server.go @@ -40,7 +40,7 @@ func init() { // Only one api server can run at the same time - this is enforced by a pidfile. // The signals SIGINT, SIGKILL and SIGTERM are intercepted for cleanup. func jobInitApi(job *engine.Job) string { - srv, err := NewServer(ConfigFromJob(job)) + srv, err := NewServer(job.Eng, ConfigFromJob(job)) if err != nil { return err.Error() } @@ -56,17 +56,19 @@ func jobInitApi(job *engine.Job) string { srv.Close() os.Exit(0) }() - err = engine.Register("serveapi", func(job *engine.Job) string { - return srv.ListenAndServe(job.Args...).Error() - }) - if err != nil { + job.Eng.Hack_SetGlobalVar("httpapi.server", srv) + if err := engine.Register("start", srv.ContainerStart); err != nil { + return err.Error() + } + if err := engine.Register("serveapi", srv.ListenAndServe); err != nil { return err.Error() } return "0" } -func (srv *Server) ListenAndServe(protoAddrs ...string) error { +func (srv *Server) ListenAndServe(job *engine.Job) string { + protoAddrs := job.Args chErrors := make(chan error, len(protoAddrs)) for _, protoAddr := range protoAddrs { protoAddrParts := strings.SplitN(protoAddr, "://", 2) @@ -80,7 +82,7 @@ func (srv *Server) ListenAndServe(protoAddrs ...string) error { log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } default: - return fmt.Errorf("Invalid protocol format.") + return "Invalid protocol format." } go func() { chErrors <- ListenAndServe(protoAddrParts[0], protoAddrParts[1], srv, true) @@ -89,10 +91,10 @@ func (srv *Server) ListenAndServe(protoAddrs ...string) error { for i := 0; i < len(protoAddrs); i += 1 { err := <-chErrors if err != nil { - return err + return err.Error() } } - return nil + return "0" } func (srv *Server) DockerVersion() APIVersion { @@ -1282,8 +1284,7 @@ func (srv *Server) RegisterLinks(name string, hostConfig *HostConfig) error { return fmt.Errorf("No such container: %s", name) } - // Register links - if hostConfig != nil && hostConfig.Links != nil { + if hostConfig.Links != nil { for _, l := range hostConfig.Links { parts, err := parseLink(l) if err != nil { @@ -1296,7 +1297,6 @@ func (srv *Server) RegisterLinks(name string, hostConfig *HostConfig) error { if child == nil { return fmt.Errorf("Could not get container for %s", parts["name"]) } - if err := runtime.RegisterLink(container, child, parts["alias"]); err != nil { return err } @@ -1312,22 +1312,36 @@ func (srv *Server) RegisterLinks(name string, hostConfig *HostConfig) error { return nil } -func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error { +func (srv *Server) ContainerStart(job *engine.Job) string { + if len(job.Args) < 1 { + return fmt.Sprintf("Usage: %s container_id", job.Name) + } + name := job.Args[0] runtime := srv.runtime container := runtime.Get(name) if container == nil { - return fmt.Errorf("No such container: %s", name) + return fmt.Sprintf("No such container: %s", name) } - if hostConfig != nil { - container.hostConfig = hostConfig + // If no environment was set, then no hostconfig was passed. + if len(job.Environ()) > 0 { + var hostConfig HostConfig + if err := job.ExportEnv(&hostConfig); err != nil { + return err.Error() + } + // Register any links from the host config before starting the container + // FIXME: we could just pass the container here, no need to lookup by name again. + if err := srv.RegisterLinks(name, &hostConfig); err != nil { + return err.Error() + } + container.hostConfig = &hostConfig container.ToDisk() } if err := container.Start(); err != nil { - return fmt.Errorf("Cannot start container %s: %s", name, err) + return fmt.Sprintf("Cannot start container %s: %s", name, err) } srv.LogEvent("start", container.ShortID(), runtime.repositories.ImageName(container.Image)) - return nil + return "0" } func (srv *Server) ContainerStop(name string, t int) error { @@ -1478,12 +1492,13 @@ func (srv *Server) ContainerCopy(name string, resource string, out io.Writer) er } -func NewServer(config *DaemonConfig) (*Server, error) { +func NewServer(eng *engine.Engine, config *DaemonConfig) (*Server, error) { runtime, err := NewRuntime(config) if err != nil { return nil, err } srv := &Server{ + Eng: eng, runtime: runtime, pullingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}), @@ -1527,4 +1542,5 @@ type Server struct { events []utils.JSONMessage listeners map[string]chan utils.JSONMessage reqFactory *utils.HTTPRequestFactory + Eng *engine.Engine } diff --git a/server_test.go b/server_test.go index 7f9cbaadf..20894f8fe 100644 --- a/server_test.go +++ b/server_test.go @@ -1,6 +1,7 @@ package docker import ( + "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/utils" "strings" "testing" @@ -109,10 +110,11 @@ func TestCreateRm(t *testing.T) { } func TestCreateRmVolumes(t *testing.T) { - runtime := mkRuntime(t) - defer nuke(runtime) + eng := engine.NewTestEngine(t) - srv := &Server{runtime: runtime} + srv := mkServerFromEngine(eng, t) + runtime := srv.runtime + defer nuke(runtime) config, hostConfig, _, err := ParseRun([]string{"-v", "/srv", GetTestImage(runtime).ID, "echo test"}, nil) if err != nil { @@ -128,8 +130,11 @@ func TestCreateRmVolumes(t *testing.T) { t.Errorf("Expected 1 container, %v found", len(runtime.List())) } - err = srv.ContainerStart(id, hostConfig) - if err != nil { + job := eng.Job("start", id) + if err := job.ImportEnv(hostConfig); err != nil { + t.Fatal(err) + } + if err := job.Run(); err != nil { t.Fatal(err) } @@ -169,11 +174,11 @@ func TestCommit(t *testing.T) { } func TestCreateStartRestartStopStartKillRm(t *testing.T) { - runtime := mkRuntime(t) + eng := engine.NewTestEngine(t) + srv := mkServerFromEngine(eng, t) + runtime := srv.runtime defer nuke(runtime) - srv := &Server{runtime: runtime} - config, hostConfig, _, err := ParseRun([]string{GetTestImage(runtime).ID, "/bin/cat"}, nil) if err != nil { t.Fatal(err) @@ -188,7 +193,11 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { t.Errorf("Expected 1 container, %v found", len(runtime.List())) } - if err := srv.ContainerStart(id, hostConfig); err != nil { + job := eng.Job("start", id) + if err := job.ImportEnv(hostConfig); err != nil { + t.Fatal(err) + } + if err := job.Run(); err != nil { t.Fatal(err) } @@ -200,7 +209,11 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { t.Fatal(err) } - if err := srv.ContainerStart(id, hostConfig); err != nil { + job = eng.Job("start", id) + if err := job.ImportEnv(hostConfig); err != nil { + t.Fatal(err) + } + if err := job.Run(); err != nil { t.Fatal(err) } @@ -384,9 +397,10 @@ func TestLogEvent(t *testing.T) { } func TestRmi(t *testing.T) { - runtime := mkRuntime(t) + eng := engine.NewTestEngine(t) + srv := mkServerFromEngine(eng, t) + runtime := srv.runtime defer nuke(runtime) - srv := &Server{runtime: runtime} initialImages, err := srv.Images(false, "") if err != nil { @@ -404,8 +418,11 @@ func TestRmi(t *testing.T) { } //To remove - err = srv.ContainerStart(containerID, hostConfig) - if err != nil { + job := eng.Job("start", containerID) + if err := job.ImportEnv(hostConfig); err != nil { + t.Fatal(err) + } + if err := job.Run(); err != nil { t.Fatal(err) } @@ -425,8 +442,11 @@ func TestRmi(t *testing.T) { } //To remove - err = srv.ContainerStart(containerID, hostConfig) - if err != nil { + job = eng.Job("start", containerID) + if err := job.ImportEnv(hostConfig); err != nil { + t.Fatal(err) + } + if err := job.Run(); err != nil { t.Fatal(err) } diff --git a/utils_test.go b/utils_test.go index 14cbd7c6b..282954285 100644 --- a/utils_test.go +++ b/utils_test.go @@ -2,6 +2,7 @@ package docker import ( "fmt" + "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/utils" "io" "io/ioutil" @@ -40,6 +41,18 @@ func mkRuntime(f Fataler) *Runtime { return runtime } +func mkServerFromEngine(eng *engine.Engine, t Fataler) *Server { + iSrv := eng.Hack_GetGlobalVar("httpapi.server") + if iSrv == nil { + t.Fatal("Legacy server field not set in engine") + } + srv, ok := iSrv.(*Server) + if !ok { + t.Fatal("Legacy server field in engine does not cast to *Server") + } + return srv +} + // A common interface to access the Fatal method of // both testing.B and testing.T. type Fataler interface {