]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blame - vendor/github.com/fsouza/go-dockerclient/testing/server.go
provider: Ensured Go 1.11 in TravisCI and README
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / fsouza / go-dockerclient / testing / server.go
CommitLineData
9b12e4fe
JC
1// Copyright 2015 go-dockerclient authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package testing provides a fake implementation of the Docker API, useful for
6// testing purpose.
7package testing
8
9import (
10 "archive/tar"
11 "crypto/rand"
12 "encoding/json"
13 "errors"
14 "fmt"
15 "io/ioutil"
16 mathrand "math/rand"
17 "net"
18 "net/http"
19 "regexp"
20 "strconv"
21 "strings"
22 "sync"
23 "time"
24
25 "github.com/fsouza/go-dockerclient"
26 "github.com/fsouza/go-dockerclient/external/github.com/docker/docker/pkg/stdcopy"
27 "github.com/fsouza/go-dockerclient/external/github.com/gorilla/mux"
28)
29
30var nameRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`)
31
32// DockerServer represents a programmable, concurrent (not much), HTTP server
33// implementing a fake version of the Docker remote API.
34//
35// It can used in standalone mode, listening for connections or as an arbitrary
36// HTTP handler.
37//
38// For more details on the remote API, check http://goo.gl/G3plxW.
39type DockerServer struct {
40 containers []*docker.Container
41 uploadedFiles map[string]string
42 execs []*docker.ExecInspect
43 execMut sync.RWMutex
44 cMut sync.RWMutex
45 images []docker.Image
46 iMut sync.RWMutex
47 imgIDs map[string]string
48 networks []*docker.Network
49 netMut sync.RWMutex
50 listener net.Listener
51 mux *mux.Router
52 hook func(*http.Request)
53 failures map[string]string
54 multiFailures []map[string]string
55 execCallbacks map[string]func()
56 statsCallbacks map[string]func(string) docker.Stats
57 customHandlers map[string]http.Handler
58 handlerMutex sync.RWMutex
59 cChan chan<- *docker.Container
60 volStore map[string]*volumeCounter
61 volMut sync.RWMutex
62}
63
64type volumeCounter struct {
65 volume docker.Volume
66 count int
67}
68
69// NewServer returns a new instance of the fake server, in standalone mode. Use
70// the method URL to get the URL of the server.
71//
72// It receives the bind address (use 127.0.0.1:0 for getting an available port
73// on the host), a channel of containers and a hook function, that will be
74// called on every request.
75//
76// The fake server will send containers in the channel whenever the container
77// changes its state, via the HTTP API (i.e.: create, start and stop). This
78// channel may be nil, which means that the server won't notify on state
79// changes.
80func NewServer(bind string, containerChan chan<- *docker.Container, hook func(*http.Request)) (*DockerServer, error) {
81 listener, err := net.Listen("tcp", bind)
82 if err != nil {
83 return nil, err
84 }
85 server := DockerServer{
86 listener: listener,
87 imgIDs: make(map[string]string),
88 hook: hook,
89 failures: make(map[string]string),
90 execCallbacks: make(map[string]func()),
91 statsCallbacks: make(map[string]func(string) docker.Stats),
92 customHandlers: make(map[string]http.Handler),
93 uploadedFiles: make(map[string]string),
94 cChan: containerChan,
95 }
96 server.buildMuxer()
97 go http.Serve(listener, &server)
98 return &server, nil
99}
100
101func (s *DockerServer) notify(container *docker.Container) {
102 if s.cChan != nil {
103 s.cChan <- container
104 }
105}
106
107func (s *DockerServer) buildMuxer() {
108 s.mux = mux.NewRouter()
109 s.mux.Path("/commit").Methods("POST").HandlerFunc(s.handlerWrapper(s.commitContainer))
110 s.mux.Path("/containers/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listContainers))
111 s.mux.Path("/containers/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createContainer))
112 s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer))
113 s.mux.Path("/containers/{id:.*}/rename").Methods("POST").HandlerFunc(s.handlerWrapper(s.renameContainer))
114 s.mux.Path("/containers/{id:.*}/top").Methods("GET").HandlerFunc(s.handlerWrapper(s.topContainer))
115 s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer))
116 s.mux.Path("/containers/{id:.*}/kill").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer))
117 s.mux.Path("/containers/{id:.*}/stop").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer))
118 s.mux.Path("/containers/{id:.*}/pause").Methods("POST").HandlerFunc(s.handlerWrapper(s.pauseContainer))
119 s.mux.Path("/containers/{id:.*}/unpause").Methods("POST").HandlerFunc(s.handlerWrapper(s.unpauseContainer))
120 s.mux.Path("/containers/{id:.*}/wait").Methods("POST").HandlerFunc(s.handlerWrapper(s.waitContainer))
121 s.mux.Path("/containers/{id:.*}/attach").Methods("POST").HandlerFunc(s.handlerWrapper(s.attachContainer))
122 s.mux.Path("/containers/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeContainer))
123 s.mux.Path("/containers/{id:.*}/exec").Methods("POST").HandlerFunc(s.handlerWrapper(s.createExecContainer))
124 s.mux.Path("/containers/{id:.*}/stats").Methods("GET").HandlerFunc(s.handlerWrapper(s.statsContainer))
125 s.mux.Path("/containers/{id:.*}/archive").Methods("PUT").HandlerFunc(s.handlerWrapper(s.uploadToContainer))
126 s.mux.Path("/exec/{id:.*}/resize").Methods("POST").HandlerFunc(s.handlerWrapper(s.resizeExecContainer))
127 s.mux.Path("/exec/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startExecContainer))
128 s.mux.Path("/exec/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectExecContainer))
129 s.mux.Path("/images/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.pullImage))
130 s.mux.Path("/build").Methods("POST").HandlerFunc(s.handlerWrapper(s.buildImage))
131 s.mux.Path("/images/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listImages))
132 s.mux.Path("/images/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeImage))
133 s.mux.Path("/images/{name:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectImage))
134 s.mux.Path("/images/{name:.*}/push").Methods("POST").HandlerFunc(s.handlerWrapper(s.pushImage))
135 s.mux.Path("/images/{name:.*}/tag").Methods("POST").HandlerFunc(s.handlerWrapper(s.tagImage))
136 s.mux.Path("/events").Methods("GET").HandlerFunc(s.listEvents)
137 s.mux.Path("/_ping").Methods("GET").HandlerFunc(s.handlerWrapper(s.pingDocker))
138 s.mux.Path("/images/load").Methods("POST").HandlerFunc(s.handlerWrapper(s.loadImage))
139 s.mux.Path("/images/{id:.*}/get").Methods("GET").HandlerFunc(s.handlerWrapper(s.getImage))
140 s.mux.Path("/networks").Methods("GET").HandlerFunc(s.handlerWrapper(s.listNetworks))
141 s.mux.Path("/networks/{id:.*}").Methods("GET").HandlerFunc(s.handlerWrapper(s.networkInfo))
142 s.mux.Path("/networks").Methods("POST").HandlerFunc(s.handlerWrapper(s.createNetwork))
143 s.mux.Path("/volumes").Methods("GET").HandlerFunc(s.handlerWrapper(s.listVolumes))
144 s.mux.Path("/volumes/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createVolume))
145 s.mux.Path("/volumes/{name:.*}").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectVolume))
146 s.mux.Path("/volumes/{name:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeVolume))
147 s.mux.Path("/info").Methods("GET").HandlerFunc(s.handlerWrapper(s.infoDocker))
148}
149
150// SetHook changes the hook function used by the server.
151//
152// The hook function is a function called on every request.
153func (s *DockerServer) SetHook(hook func(*http.Request)) {
154 s.hook = hook
155}
156
157// PrepareExec adds a callback to a container exec in the fake server.
158//
159// This function will be called whenever the given exec id is started, and the
160// given exec id will remain in the "Running" start while the function is
161// running, so it's useful for emulating an exec that runs for two seconds, for
162// example:
163//
164// opts := docker.CreateExecOptions{
165// AttachStdin: true,
166// AttachStdout: true,
167// AttachStderr: true,
168// Tty: true,
169// Cmd: []string{"/bin/bash", "-l"},
170// }
171// // Client points to a fake server.
172// exec, err := client.CreateExec(opts)
173// // handle error
174// server.PrepareExec(exec.ID, func() {time.Sleep(2 * time.Second)})
175// err = client.StartExec(exec.ID, docker.StartExecOptions{Tty: true}) // will block for 2 seconds
176// // handle error
177func (s *DockerServer) PrepareExec(id string, callback func()) {
178 s.execCallbacks[id] = callback
179}
180
181// PrepareStats adds a callback that will be called for each container stats
182// call.
183//
184// This callback function will be called multiple times if stream is set to
185// true when stats is called.
186func (s *DockerServer) PrepareStats(id string, callback func(string) docker.Stats) {
187 s.statsCallbacks[id] = callback
188}
189
190// PrepareFailure adds a new expected failure based on a URL regexp it receives
191// an id for the failure.
192func (s *DockerServer) PrepareFailure(id string, urlRegexp string) {
193 s.failures[id] = urlRegexp
194}
195
196// PrepareMultiFailures enqueues a new expected failure based on a URL regexp
197// it receives an id for the failure.
198func (s *DockerServer) PrepareMultiFailures(id string, urlRegexp string) {
199 s.multiFailures = append(s.multiFailures, map[string]string{"error": id, "url": urlRegexp})
200}
201
202// ResetFailure removes an expected failure identified by the given id.
203func (s *DockerServer) ResetFailure(id string) {
204 delete(s.failures, id)
205}
206
207// ResetMultiFailures removes all enqueued failures.
208func (s *DockerServer) ResetMultiFailures() {
209 s.multiFailures = []map[string]string{}
210}
211
212// CustomHandler registers a custom handler for a specific path.
213//
214// For example:
215//
216// server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
217// http.Error(w, "Something wrong is not right", http.StatusInternalServerError)
218// }))
219func (s *DockerServer) CustomHandler(path string, handler http.Handler) {
220 s.handlerMutex.Lock()
221 s.customHandlers[path] = handler
222 s.handlerMutex.Unlock()
223}
224
225// MutateContainer changes the state of a container, returning an error if the
226// given id does not match to any container "running" in the server.
227func (s *DockerServer) MutateContainer(id string, state docker.State) error {
228 for _, container := range s.containers {
229 if container.ID == id {
230 container.State = state
231 return nil
232 }
233 }
234 return errors.New("container not found")
235}
236
237// Stop stops the server.
238func (s *DockerServer) Stop() {
239 if s.listener != nil {
240 s.listener.Close()
241 }
242}
243
244// URL returns the HTTP URL of the server.
245func (s *DockerServer) URL() string {
246 if s.listener == nil {
247 return ""
248 }
249 return "http://" + s.listener.Addr().String() + "/"
250}
251
252// ServeHTTP handles HTTP requests sent to the server.
253func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
254 s.handlerMutex.RLock()
255 defer s.handlerMutex.RUnlock()
256 for re, handler := range s.customHandlers {
257 if m, _ := regexp.MatchString(re, r.URL.Path); m {
258 handler.ServeHTTP(w, r)
259 return
260 }
261 }
262 s.mux.ServeHTTP(w, r)
263 if s.hook != nil {
264 s.hook(r)
265 }
266}
267
268// DefaultHandler returns default http.Handler mux, it allows customHandlers to
269// call the default behavior if wanted.
270func (s *DockerServer) DefaultHandler() http.Handler {
271 return s.mux
272}
273
274func (s *DockerServer) handlerWrapper(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
275 return func(w http.ResponseWriter, r *http.Request) {
276 for errorID, urlRegexp := range s.failures {
277 matched, err := regexp.MatchString(urlRegexp, r.URL.Path)
278 if err != nil {
279 http.Error(w, err.Error(), http.StatusBadRequest)
280 return
281 }
282 if !matched {
283 continue
284 }
285 http.Error(w, errorID, http.StatusBadRequest)
286 return
287 }
288 for i, failure := range s.multiFailures {
289 matched, err := regexp.MatchString(failure["url"], r.URL.Path)
290 if err != nil {
291 http.Error(w, err.Error(), http.StatusBadRequest)
292 return
293 }
294 if !matched {
295 continue
296 }
297 http.Error(w, failure["error"], http.StatusBadRequest)
298 s.multiFailures = append(s.multiFailures[:i], s.multiFailures[i+1:]...)
299 return
300 }
301 f(w, r)
302 }
303}
304
305func (s *DockerServer) listContainers(w http.ResponseWriter, r *http.Request) {
306 all := r.URL.Query().Get("all")
307 s.cMut.RLock()
308 result := make([]docker.APIContainers, 0, len(s.containers))
309 for _, container := range s.containers {
310 if all == "1" || container.State.Running {
311 result = append(result, docker.APIContainers{
312 ID: container.ID,
313 Image: container.Image,
314 Command: fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")),
315 Created: container.Created.Unix(),
316 Status: container.State.String(),
317 Ports: container.NetworkSettings.PortMappingAPI(),
318 Names: []string{fmt.Sprintf("/%s", container.Name)},
319 })
320 }
321 }
322 s.cMut.RUnlock()
323 w.Header().Set("Content-Type", "application/json")
324 w.WriteHeader(http.StatusOK)
325 json.NewEncoder(w).Encode(result)
326}
327
328func (s *DockerServer) listImages(w http.ResponseWriter, r *http.Request) {
329 s.cMut.RLock()
330 result := make([]docker.APIImages, len(s.images))
331 for i, image := range s.images {
332 result[i] = docker.APIImages{
333 ID: image.ID,
334 Created: image.Created.Unix(),
335 }
336 for tag, id := range s.imgIDs {
337 if id == image.ID {
338 result[i].RepoTags = append(result[i].RepoTags, tag)
339 }
340 }
341 }
342 s.cMut.RUnlock()
343 w.Header().Set("Content-Type", "application/json")
344 w.WriteHeader(http.StatusOK)
345 json.NewEncoder(w).Encode(result)
346}
347
348func (s *DockerServer) findImage(id string) (string, error) {
349 s.iMut.RLock()
350 defer s.iMut.RUnlock()
351 image, ok := s.imgIDs[id]
352 if ok {
353 return image, nil
354 }
355 image, _, err := s.findImageByID(id)
356 return image, err
357}
358
359func (s *DockerServer) findImageByID(id string) (string, int, error) {
360 s.iMut.RLock()
361 defer s.iMut.RUnlock()
362 for i, image := range s.images {
363 if image.ID == id {
364 return image.ID, i, nil
365 }
366 }
367 return "", -1, errors.New("No such image")
368}
369
370func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) {
371 var config struct {
372 *docker.Config
373 HostConfig *docker.HostConfig
374 }
375 defer r.Body.Close()
376 err := json.NewDecoder(r.Body).Decode(&config)
377 if err != nil {
378 http.Error(w, err.Error(), http.StatusBadRequest)
379 return
380 }
381 name := r.URL.Query().Get("name")
382 if name != "" && !nameRegexp.MatchString(name) {
383 http.Error(w, "Invalid container name", http.StatusInternalServerError)
384 return
385 }
386 if _, err := s.findImage(config.Image); err != nil {
387 http.Error(w, err.Error(), http.StatusNotFound)
388 return
389 }
390 ports := map[docker.Port][]docker.PortBinding{}
391 for port := range config.ExposedPorts {
392 ports[port] = []docker.PortBinding{{
393 HostIP: "0.0.0.0",
394 HostPort: strconv.Itoa(mathrand.Int() % 0xffff),
395 }}
396 }
397
398 //the container may not have cmd when using a Dockerfile
399 var path string
400 var args []string
401 if len(config.Cmd) == 1 {
402 path = config.Cmd[0]
403 } else if len(config.Cmd) > 1 {
404 path = config.Cmd[0]
405 args = config.Cmd[1:]
406 }
407
408 generatedID := s.generateID()
409 config.Config.Hostname = generatedID[:12]
410 container := docker.Container{
411 Name: name,
412 ID: generatedID,
413 Created: time.Now(),
414 Path: path,
415 Args: args,
416 Config: config.Config,
417 HostConfig: config.HostConfig,
418 State: docker.State{
419 Running: false,
420 Pid: mathrand.Int() % 50000,
421 ExitCode: 0,
422 StartedAt: time.Now(),
423 },
424 Image: config.Image,
425 NetworkSettings: &docker.NetworkSettings{
426 IPAddress: fmt.Sprintf("172.16.42.%d", mathrand.Int()%250+2),
427 IPPrefixLen: 24,
428 Gateway: "172.16.42.1",
429 Bridge: "docker0",
430 Ports: ports,
431 },
432 }
433 s.cMut.Lock()
434 if container.Name != "" {
435 for _, c := range s.containers {
436 if c.Name == container.Name {
437 defer s.cMut.Unlock()
438 http.Error(w, "there's already a container with this name", http.StatusConflict)
439 return
440 }
441 }
442 }
443 s.containers = append(s.containers, &container)
444 s.cMut.Unlock()
445 w.WriteHeader(http.StatusCreated)
446 s.notify(&container)
447
448 json.NewEncoder(w).Encode(container)
449}
450
451func (s *DockerServer) generateID() string {
452 var buf [16]byte
453 rand.Read(buf[:])
454 return fmt.Sprintf("%x", buf)
455}
456
457func (s *DockerServer) renameContainer(w http.ResponseWriter, r *http.Request) {
458 id := mux.Vars(r)["id"]
459 container, index, err := s.findContainer(id)
460 if err != nil {
461 http.Error(w, err.Error(), http.StatusNotFound)
462 return
463 }
464 copy := *container
465 copy.Name = r.URL.Query().Get("name")
466 s.cMut.Lock()
467 defer s.cMut.Unlock()
468 if s.containers[index].ID == copy.ID {
469 s.containers[index] = &copy
470 }
471 w.WriteHeader(http.StatusNoContent)
472}
473
474func (s *DockerServer) inspectContainer(w http.ResponseWriter, r *http.Request) {
475 id := mux.Vars(r)["id"]
476 container, _, err := s.findContainer(id)
477 if err != nil {
478 http.Error(w, err.Error(), http.StatusNotFound)
479 return
480 }
481 w.Header().Set("Content-Type", "application/json")
482 w.WriteHeader(http.StatusOK)
483 json.NewEncoder(w).Encode(container)
484}
485
486func (s *DockerServer) statsContainer(w http.ResponseWriter, r *http.Request) {
487 id := mux.Vars(r)["id"]
488 _, _, err := s.findContainer(id)
489 if err != nil {
490 http.Error(w, err.Error(), http.StatusNotFound)
491 return
492 }
493 stream, _ := strconv.ParseBool(r.URL.Query().Get("stream"))
494 callback := s.statsCallbacks[id]
495 w.Header().Set("Content-Type", "application/json")
496 w.WriteHeader(http.StatusOK)
497 encoder := json.NewEncoder(w)
498 for {
499 var stats docker.Stats
500 if callback != nil {
501 stats = callback(id)
502 }
503 encoder.Encode(stats)
504 if !stream {
505 break
506 }
507 }
508}
509
510func (s *DockerServer) uploadToContainer(w http.ResponseWriter, r *http.Request) {
511 id := mux.Vars(r)["id"]
512 container, _, err := s.findContainer(id)
513 if err != nil {
514 http.Error(w, err.Error(), http.StatusNotFound)
515 return
516 }
517 if !container.State.Running {
518 w.WriteHeader(http.StatusInternalServerError)
519 fmt.Fprintf(w, "Container %s is not running", id)
520 return
521 }
522 path := r.URL.Query().Get("path")
523 s.uploadedFiles[id] = path
524 w.WriteHeader(http.StatusOK)
525}
526
527func (s *DockerServer) topContainer(w http.ResponseWriter, r *http.Request) {
528 id := mux.Vars(r)["id"]
529 container, _, err := s.findContainer(id)
530 if err != nil {
531 http.Error(w, err.Error(), http.StatusNotFound)
532 return
533 }
534 if !container.State.Running {
535 w.WriteHeader(http.StatusInternalServerError)
536 fmt.Fprintf(w, "Container %s is not running", id)
537 return
538 }
539 w.Header().Set("Content-Type", "application/json")
540 w.WriteHeader(http.StatusOK)
541 result := docker.TopResult{
542 Titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
543 Processes: [][]string{
544 {"root", "7535", "7516", "0", "03:20", "?", "00:00:00", container.Path + " " + strings.Join(container.Args, " ")},
545 },
546 }
547 json.NewEncoder(w).Encode(result)
548}
549
550func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) {
551 id := mux.Vars(r)["id"]
552 container, _, err := s.findContainer(id)
553 if err != nil {
554 http.Error(w, err.Error(), http.StatusNotFound)
555 return
556 }
557 s.cMut.Lock()
558 defer s.cMut.Unlock()
559 defer r.Body.Close()
560 var hostConfig docker.HostConfig
561 err = json.NewDecoder(r.Body).Decode(&hostConfig)
562 if err != nil {
563 http.Error(w, err.Error(), http.StatusInternalServerError)
564 return
565 }
566 container.HostConfig = &hostConfig
567 if len(hostConfig.PortBindings) > 0 {
568 ports := map[docker.Port][]docker.PortBinding{}
569 for key, items := range hostConfig.PortBindings {
570 bindings := make([]docker.PortBinding, len(items))
571 for i := range items {
572 binding := docker.PortBinding{
573 HostIP: items[i].HostIP,
574 HostPort: items[i].HostPort,
575 }
576 if binding.HostIP == "" {
577 binding.HostIP = "0.0.0.0"
578 }
579 if binding.HostPort == "" {
580 binding.HostPort = strconv.Itoa(mathrand.Int() % 0xffff)
581 }
582 bindings[i] = binding
583 }
584 ports[key] = bindings
585 }
586 container.NetworkSettings.Ports = ports
587 }
588 if container.State.Running {
589 http.Error(w, "", http.StatusNotModified)
590 return
591 }
592 container.State.Running = true
593 s.notify(container)
594}
595
596func (s *DockerServer) stopContainer(w http.ResponseWriter, r *http.Request) {
597 id := mux.Vars(r)["id"]
598 container, _, err := s.findContainer(id)
599 if err != nil {
600 http.Error(w, err.Error(), http.StatusNotFound)
601 return
602 }
603 s.cMut.Lock()
604 defer s.cMut.Unlock()
605 if !container.State.Running {
606 http.Error(w, "Container not running", http.StatusBadRequest)
607 return
608 }
609 w.WriteHeader(http.StatusNoContent)
610 container.State.Running = false
611 s.notify(container)
612}
613
614func (s *DockerServer) pauseContainer(w http.ResponseWriter, r *http.Request) {
615 id := mux.Vars(r)["id"]
616 container, _, err := s.findContainer(id)
617 if err != nil {
618 http.Error(w, err.Error(), http.StatusNotFound)
619 return
620 }
621 s.cMut.Lock()
622 defer s.cMut.Unlock()
623 if container.State.Paused {
624 http.Error(w, "Container already paused", http.StatusBadRequest)
625 return
626 }
627 w.WriteHeader(http.StatusNoContent)
628 container.State.Paused = true
629}
630
631func (s *DockerServer) unpauseContainer(w http.ResponseWriter, r *http.Request) {
632 id := mux.Vars(r)["id"]
633 container, _, err := s.findContainer(id)
634 if err != nil {
635 http.Error(w, err.Error(), http.StatusNotFound)
636 return
637 }
638 s.cMut.Lock()
639 defer s.cMut.Unlock()
640 if !container.State.Paused {
641 http.Error(w, "Container not paused", http.StatusBadRequest)
642 return
643 }
644 w.WriteHeader(http.StatusNoContent)
645 container.State.Paused = false
646}
647
648func (s *DockerServer) attachContainer(w http.ResponseWriter, r *http.Request) {
649 id := mux.Vars(r)["id"]
650 container, _, err := s.findContainer(id)
651 if err != nil {
652 http.Error(w, err.Error(), http.StatusNotFound)
653 return
654 }
655 hijacker, ok := w.(http.Hijacker)
656 if !ok {
657 http.Error(w, "cannot hijack connection", http.StatusInternalServerError)
658 return
659 }
660 w.Header().Set("Content-Type", "application/vnd.docker.raw-stream")
661 w.WriteHeader(http.StatusOK)
662 conn, _, err := hijacker.Hijack()
663 if err != nil {
664 http.Error(w, err.Error(), http.StatusInternalServerError)
665 return
666 }
667 wg := sync.WaitGroup{}
668 if r.URL.Query().Get("stdin") == "1" {
669 wg.Add(1)
670 go func() {
671 ioutil.ReadAll(conn)
672 wg.Done()
673 }()
674 }
675 outStream := stdcopy.NewStdWriter(conn, stdcopy.Stdout)
676 if container.State.Running {
677 fmt.Fprintf(outStream, "Container is running\n")
678 } else {
679 fmt.Fprintf(outStream, "Container is not running\n")
680 }
681 fmt.Fprintln(outStream, "What happened?")
682 fmt.Fprintln(outStream, "Something happened")
683 wg.Wait()
684 if r.URL.Query().Get("stream") == "1" {
685 for {
686 time.Sleep(1e6)
687 s.cMut.RLock()
688 if !container.State.Running {
689 s.cMut.RUnlock()
690 break
691 }
692 s.cMut.RUnlock()
693 }
694 }
695 conn.Close()
696}
697
698func (s *DockerServer) waitContainer(w http.ResponseWriter, r *http.Request) {
699 id := mux.Vars(r)["id"]
700 container, _, err := s.findContainer(id)
701 if err != nil {
702 http.Error(w, err.Error(), http.StatusNotFound)
703 return
704 }
705 for {
706 time.Sleep(1e6)
707 s.cMut.RLock()
708 if !container.State.Running {
709 s.cMut.RUnlock()
710 break
711 }
712 s.cMut.RUnlock()
713 }
714 result := map[string]int{"StatusCode": container.State.ExitCode}
715 json.NewEncoder(w).Encode(result)
716}
717
718func (s *DockerServer) removeContainer(w http.ResponseWriter, r *http.Request) {
719 id := mux.Vars(r)["id"]
720 force := r.URL.Query().Get("force")
721 s.cMut.Lock()
722 defer s.cMut.Unlock()
723 container, index, err := s.findContainerWithLock(id, false)
724 if err != nil {
725 http.Error(w, err.Error(), http.StatusNotFound)
726 return
727 }
728 if container.State.Running && force != "1" {
729 msg := "Error: API error (406): Impossible to remove a running container, please stop it first"
730 http.Error(w, msg, http.StatusInternalServerError)
731 return
732 }
733 w.WriteHeader(http.StatusNoContent)
734 s.containers[index] = s.containers[len(s.containers)-1]
735 s.containers = s.containers[:len(s.containers)-1]
736}
737
738func (s *DockerServer) commitContainer(w http.ResponseWriter, r *http.Request) {
739 id := r.URL.Query().Get("container")
740 container, _, err := s.findContainer(id)
741 if err != nil {
742 http.Error(w, err.Error(), http.StatusNotFound)
743 return
744 }
745 config := new(docker.Config)
746 runConfig := r.URL.Query().Get("run")
747 if runConfig != "" {
748 err = json.Unmarshal([]byte(runConfig), config)
749 if err != nil {
750 http.Error(w, err.Error(), http.StatusBadRequest)
751 return
752 }
753 }
754 w.WriteHeader(http.StatusOK)
755 image := docker.Image{
756 ID: "img-" + container.ID,
757 Parent: container.Image,
758 Container: container.ID,
759 Comment: r.URL.Query().Get("m"),
760 Author: r.URL.Query().Get("author"),
761 Config: config,
762 }
763 repository := r.URL.Query().Get("repo")
764 tag := r.URL.Query().Get("tag")
765 s.iMut.Lock()
766 s.images = append(s.images, image)
767 if repository != "" {
768 if tag != "" {
769 repository += ":" + tag
770 }
771 s.imgIDs[repository] = image.ID
772 }
773 s.iMut.Unlock()
774 fmt.Fprintf(w, `{"ID":%q}`, image.ID)
775}
776
777func (s *DockerServer) findContainer(idOrName string) (*docker.Container, int, error) {
778 return s.findContainerWithLock(idOrName, true)
779}
780
781func (s *DockerServer) findContainerWithLock(idOrName string, shouldLock bool) (*docker.Container, int, error) {
782 if shouldLock {
783 s.cMut.RLock()
784 defer s.cMut.RUnlock()
785 }
786 for i, container := range s.containers {
787 if container.ID == idOrName || container.Name == idOrName {
788 return container, i, nil
789 }
790 }
791 return nil, -1, errors.New("No such container")
792}
793
794func (s *DockerServer) buildImage(w http.ResponseWriter, r *http.Request) {
795 if ct := r.Header.Get("Content-Type"); ct == "application/tar" {
796 gotDockerFile := false
797 tr := tar.NewReader(r.Body)
798 for {
799 header, err := tr.Next()
800 if err != nil {
801 break
802 }
803 if header.Name == "Dockerfile" {
804 gotDockerFile = true
805 }
806 }
807 if !gotDockerFile {
808 w.WriteHeader(http.StatusBadRequest)
809 w.Write([]byte("miss Dockerfile"))
810 return
811 }
812 }
813 //we did not use that Dockerfile to build image cause we are a fake Docker daemon
814 image := docker.Image{
815 ID: s.generateID(),
816 Created: time.Now(),
817 }
818
819 query := r.URL.Query()
820 repository := image.ID
821 if t := query.Get("t"); t != "" {
822 repository = t
823 }
824 s.iMut.Lock()
825 s.images = append(s.images, image)
826 s.imgIDs[repository] = image.ID
827 s.iMut.Unlock()
828 w.Write([]byte(fmt.Sprintf("Successfully built %s", image.ID)))
829}
830
831func (s *DockerServer) pullImage(w http.ResponseWriter, r *http.Request) {
832 fromImageName := r.URL.Query().Get("fromImage")
833 tag := r.URL.Query().Get("tag")
834 image := docker.Image{
835 ID: s.generateID(),
836 Config: &docker.Config{},
837 }
838 s.iMut.Lock()
839 s.images = append(s.images, image)
840 if fromImageName != "" {
841 if tag != "" {
842 fromImageName = fmt.Sprintf("%s:%s", fromImageName, tag)
843 }
844 s.imgIDs[fromImageName] = image.ID
845 }
846 s.iMut.Unlock()
847}
848
849func (s *DockerServer) pushImage(w http.ResponseWriter, r *http.Request) {
850 name := mux.Vars(r)["name"]
851 tag := r.URL.Query().Get("tag")
852 if tag != "" {
853 name += ":" + tag
854 }
855 s.iMut.RLock()
856 if _, ok := s.imgIDs[name]; !ok {
857 s.iMut.RUnlock()
858 http.Error(w, "No such image", http.StatusNotFound)
859 return
860 }
861 s.iMut.RUnlock()
862 fmt.Fprintln(w, "Pushing...")
863 fmt.Fprintln(w, "Pushed")
864}
865
866func (s *DockerServer) tagImage(w http.ResponseWriter, r *http.Request) {
867 name := mux.Vars(r)["name"]
868 s.iMut.RLock()
869 if _, ok := s.imgIDs[name]; !ok {
870 s.iMut.RUnlock()
871 http.Error(w, "No such image", http.StatusNotFound)
872 return
873 }
874 s.iMut.RUnlock()
875 s.iMut.Lock()
876 defer s.iMut.Unlock()
877 newRepo := r.URL.Query().Get("repo")
878 newTag := r.URL.Query().Get("tag")
879 if newTag != "" {
880 newRepo += ":" + newTag
881 }
882 s.imgIDs[newRepo] = s.imgIDs[name]
883 w.WriteHeader(http.StatusCreated)
884}
885
886func (s *DockerServer) removeImage(w http.ResponseWriter, r *http.Request) {
887 id := mux.Vars(r)["id"]
888 s.iMut.RLock()
889 var tag string
890 if img, ok := s.imgIDs[id]; ok {
891 id, tag = img, id
892 }
893 var tags []string
894 for tag, taggedID := range s.imgIDs {
895 if taggedID == id {
896 tags = append(tags, tag)
897 }
898 }
899 s.iMut.RUnlock()
900 _, index, err := s.findImageByID(id)
901 if err != nil {
902 http.Error(w, err.Error(), http.StatusNotFound)
903 return
904 }
905 w.WriteHeader(http.StatusNoContent)
906 s.iMut.Lock()
907 defer s.iMut.Unlock()
908 if len(tags) < 2 {
909 s.images[index] = s.images[len(s.images)-1]
910 s.images = s.images[:len(s.images)-1]
911 }
912 if tag != "" {
913 delete(s.imgIDs, tag)
914 }
915}
916
917func (s *DockerServer) inspectImage(w http.ResponseWriter, r *http.Request) {
918 name := mux.Vars(r)["name"]
919 s.iMut.RLock()
920 defer s.iMut.RUnlock()
921 if id, ok := s.imgIDs[name]; ok {
922 for _, img := range s.images {
923 if img.ID == id {
924 w.Header().Set("Content-Type", "application/json")
925 w.WriteHeader(http.StatusOK)
926 json.NewEncoder(w).Encode(img)
927 return
928 }
929 }
930 }
931 http.Error(w, "not found", http.StatusNotFound)
932}
933
934func (s *DockerServer) listEvents(w http.ResponseWriter, r *http.Request) {
935 w.Header().Set("Content-Type", "application/json")
936 var events [][]byte
937 count := mathrand.Intn(20)
938 for i := 0; i < count; i++ {
939 data, err := json.Marshal(s.generateEvent())
940 if err != nil {
941 w.WriteHeader(http.StatusInternalServerError)
942 return
943 }
944 events = append(events, data)
945 }
946 w.WriteHeader(http.StatusOK)
947 for _, d := range events {
948 fmt.Fprintln(w, d)
949 time.Sleep(time.Duration(mathrand.Intn(200)) * time.Millisecond)
950 }
951}
952
953func (s *DockerServer) pingDocker(w http.ResponseWriter, r *http.Request) {
954 w.WriteHeader(http.StatusOK)
955}
956
957func (s *DockerServer) generateEvent() *docker.APIEvents {
958 var eventType string
959 switch mathrand.Intn(4) {
960 case 0:
961 eventType = "create"
962 case 1:
963 eventType = "start"
964 case 2:
965 eventType = "stop"
966 case 3:
967 eventType = "destroy"
968 }
969 return &docker.APIEvents{
970 ID: s.generateID(),
971 Status: eventType,
972 From: "mybase:latest",
973 Time: time.Now().Unix(),
974 }
975}
976
977func (s *DockerServer) loadImage(w http.ResponseWriter, r *http.Request) {
978 w.WriteHeader(http.StatusOK)
979}
980
981func (s *DockerServer) getImage(w http.ResponseWriter, r *http.Request) {
982 w.WriteHeader(http.StatusOK)
983 w.Header().Set("Content-Type", "application/tar")
984}
985
986func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Request) {
987 id := mux.Vars(r)["id"]
988 container, _, err := s.findContainer(id)
989 if err != nil {
990 http.Error(w, err.Error(), http.StatusNotFound)
991 return
992 }
993
994 execID := s.generateID()
995 container.ExecIDs = append(container.ExecIDs, execID)
996
997 exec := docker.ExecInspect{
998 ID: execID,
999 Container: *container,
1000 }
1001
1002 var params docker.CreateExecOptions
1003 err = json.NewDecoder(r.Body).Decode(&params)
1004 if err != nil {
1005 http.Error(w, err.Error(), http.StatusInternalServerError)
1006 return
1007 }
1008 if len(params.Cmd) > 0 {
1009 exec.ProcessConfig.EntryPoint = params.Cmd[0]
1010 if len(params.Cmd) > 1 {
1011 exec.ProcessConfig.Arguments = params.Cmd[1:]
1012 }
1013 }
1014
1015 exec.ProcessConfig.User = params.User
1016 exec.ProcessConfig.Tty = params.Tty
1017
1018 s.execMut.Lock()
1019 s.execs = append(s.execs, &exec)
1020 s.execMut.Unlock()
1021 w.WriteHeader(http.StatusOK)
1022 w.Header().Set("Content-Type", "application/json")
1023 json.NewEncoder(w).Encode(map[string]string{"Id": exec.ID})
1024}
1025
1026func (s *DockerServer) startExecContainer(w http.ResponseWriter, r *http.Request) {
1027 id := mux.Vars(r)["id"]
1028 if exec, err := s.getExec(id, false); err == nil {
1029 s.execMut.Lock()
1030 exec.Running = true
1031 s.execMut.Unlock()
1032 if callback, ok := s.execCallbacks[id]; ok {
1033 callback()
1034 delete(s.execCallbacks, id)
1035 } else if callback, ok := s.execCallbacks["*"]; ok {
1036 callback()
1037 delete(s.execCallbacks, "*")
1038 }
1039 s.execMut.Lock()
1040 exec.Running = false
1041 s.execMut.Unlock()
1042 w.WriteHeader(http.StatusOK)
1043 return
1044 }
1045 w.WriteHeader(http.StatusNotFound)
1046}
1047
1048func (s *DockerServer) resizeExecContainer(w http.ResponseWriter, r *http.Request) {
1049 id := mux.Vars(r)["id"]
1050 if _, err := s.getExec(id, false); err == nil {
1051 w.WriteHeader(http.StatusOK)
1052 return
1053 }
1054 w.WriteHeader(http.StatusNotFound)
1055}
1056
1057func (s *DockerServer) inspectExecContainer(w http.ResponseWriter, r *http.Request) {
1058 id := mux.Vars(r)["id"]
1059 if exec, err := s.getExec(id, true); err == nil {
1060 w.WriteHeader(http.StatusOK)
1061 w.Header().Set("Content-Type", "application/json")
1062 json.NewEncoder(w).Encode(exec)
1063 return
1064 }
1065 w.WriteHeader(http.StatusNotFound)
1066}
1067
1068func (s *DockerServer) getExec(id string, copy bool) (*docker.ExecInspect, error) {
1069 s.execMut.RLock()
1070 defer s.execMut.RUnlock()
1071 for _, exec := range s.execs {
1072 if exec.ID == id {
1073 if copy {
1074 cp := *exec
1075 exec = &cp
1076 }
1077 return exec, nil
1078 }
1079 }
1080 return nil, errors.New("exec not found")
1081}
1082
1083func (s *DockerServer) findNetwork(idOrName string) (*docker.Network, int, error) {
1084 s.netMut.RLock()
1085 defer s.netMut.RUnlock()
1086 for i, network := range s.networks {
1087 if network.ID == idOrName || network.Name == idOrName {
1088 return network, i, nil
1089 }
1090 }
1091 return nil, -1, errors.New("No such network")
1092}
1093
1094func (s *DockerServer) listNetworks(w http.ResponseWriter, r *http.Request) {
1095 s.netMut.RLock()
1096 result := make([]docker.Network, 0, len(s.networks))
1097 for _, network := range s.networks {
1098 result = append(result, *network)
1099 }
1100 s.netMut.RUnlock()
1101 w.Header().Set("Content-Type", "application/json")
1102 w.WriteHeader(http.StatusOK)
1103 json.NewEncoder(w).Encode(result)
1104}
1105
1106func (s *DockerServer) networkInfo(w http.ResponseWriter, r *http.Request) {
1107 id := mux.Vars(r)["id"]
1108 network, _, err := s.findNetwork(id)
1109 if err != nil {
1110 http.Error(w, err.Error(), http.StatusNotFound)
1111 return
1112 }
1113 w.Header().Set("Content-Type", "application/json")
1114 w.WriteHeader(http.StatusOK)
1115 json.NewEncoder(w).Encode(network)
1116}
1117
1118// isValidName validates configuration objects supported by libnetwork
1119func isValidName(name string) bool {
1120 if name == "" || strings.Contains(name, ".") {
1121 return false
1122 }
1123 return true
1124}
1125
1126func (s *DockerServer) createNetwork(w http.ResponseWriter, r *http.Request) {
1127 var config *docker.CreateNetworkOptions
1128 defer r.Body.Close()
1129 err := json.NewDecoder(r.Body).Decode(&config)
1130 if err != nil {
1131 http.Error(w, err.Error(), http.StatusBadRequest)
1132 return
1133 }
1134 if !isValidName(config.Name) {
1135 http.Error(w, "Invalid network name", http.StatusBadRequest)
1136 return
1137 }
1138 if n, _, _ := s.findNetwork(config.Name); n != nil {
1139 http.Error(w, "network already exists", http.StatusForbidden)
1140 return
1141 }
1142
1143 generatedID := s.generateID()
1144 network := docker.Network{
1145 Name: config.Name,
1146 ID: generatedID,
1147 Driver: config.Driver,
1148 }
1149 s.netMut.Lock()
1150 s.networks = append(s.networks, &network)
1151 s.netMut.Unlock()
1152 w.WriteHeader(http.StatusCreated)
1153 var c = struct{ ID string }{ID: network.ID}
1154 json.NewEncoder(w).Encode(c)
1155}
1156
1157func (s *DockerServer) listVolumes(w http.ResponseWriter, r *http.Request) {
1158 s.volMut.RLock()
1159 result := make([]docker.Volume, 0, len(s.volStore))
1160 for _, volumeCounter := range s.volStore {
1161 result = append(result, volumeCounter.volume)
1162 }
1163 s.volMut.RUnlock()
1164 w.Header().Set("Content-Type", "application/json")
1165 w.WriteHeader(http.StatusOK)
1166 json.NewEncoder(w).Encode(result)
1167}
1168
1169func (s *DockerServer) createVolume(w http.ResponseWriter, r *http.Request) {
1170 var data struct {
1171 *docker.CreateVolumeOptions
1172 }
1173 defer r.Body.Close()
1174 err := json.NewDecoder(r.Body).Decode(&data)
1175 if err != nil {
1176 http.Error(w, err.Error(), http.StatusBadRequest)
1177 return
1178 }
1179 volume := &docker.Volume{
1180 Name: data.CreateVolumeOptions.Name,
1181 Driver: data.CreateVolumeOptions.Driver,
1182 }
1183 // If the name is not specified, generate one. Just using generateID for now
1184 if len(volume.Name) == 0 {
1185 volume.Name = s.generateID()
1186 }
1187 // If driver is not specified, use local
1188 if len(volume.Driver) == 0 {
1189 volume.Driver = "local"
1190 }
1191 // Mount point is a default one with name
1192 volume.Mountpoint = "/var/lib/docker/volumes/" + volume.Name
1193
1194 // If the volume already exists, don't re-add it.
1195 exists := false
1196 s.volMut.Lock()
1197 if s.volStore != nil {
1198 _, exists = s.volStore[volume.Name]
1199 } else {
1200 // No volumes, create volStore
1201 s.volStore = make(map[string]*volumeCounter)
1202 }
1203 if !exists {
1204 s.volStore[volume.Name] = &volumeCounter{
1205 volume: *volume,
1206 count: 0,
1207 }
1208 }
1209 s.volMut.Unlock()
1210 w.WriteHeader(http.StatusCreated)
1211 json.NewEncoder(w).Encode(volume)
1212}
1213
1214func (s *DockerServer) inspectVolume(w http.ResponseWriter, r *http.Request) {
1215 s.volMut.RLock()
1216 defer s.volMut.RUnlock()
1217 name := mux.Vars(r)["name"]
1218 vol, err := s.findVolume(name)
1219 if err != nil {
1220 http.Error(w, err.Error(), http.StatusNotFound)
1221 return
1222 }
1223 w.Header().Set("Content-Type", "application/json")
1224 w.WriteHeader(http.StatusOK)
1225 json.NewEncoder(w).Encode(vol.volume)
1226}
1227
1228func (s *DockerServer) findVolume(name string) (*volumeCounter, error) {
1229 vol, ok := s.volStore[name]
1230 if !ok {
1231 return nil, errors.New("no such volume")
1232 }
1233 return vol, nil
1234}
1235
1236func (s *DockerServer) removeVolume(w http.ResponseWriter, r *http.Request) {
1237 s.volMut.Lock()
1238 defer s.volMut.Unlock()
1239 name := mux.Vars(r)["name"]
1240 vol, err := s.findVolume(name)
1241 if err != nil {
1242 http.Error(w, err.Error(), http.StatusNotFound)
1243 return
1244 }
1245 if vol.count != 0 {
1246 http.Error(w, "volume in use and cannot be removed", http.StatusConflict)
1247 return
1248 }
1249 s.volStore[vol.volume.Name] = nil
1250 w.WriteHeader(http.StatusNoContent)
1251}
1252
1253func (s *DockerServer) infoDocker(w http.ResponseWriter, r *http.Request) {
1254 s.cMut.RLock()
1255 defer s.cMut.RUnlock()
1256 s.iMut.RLock()
1257 defer s.iMut.RUnlock()
1258 var running, stopped, paused int
1259 for _, c := range s.containers {
1260 if c.State.Running {
1261 running++
1262 } else {
1263 stopped++
1264 }
1265 if c.State.Paused {
1266 paused++
1267 }
1268 }
1269 envs := map[string]interface{}{
1270 "ID": "AAAA:XXXX:0000:BBBB:AAAA:XXXX:0000:BBBB:AAAA:XXXX:0000:BBBB",
1271 "Containers": len(s.containers),
1272 "ContainersRunning": running,
1273 "ContainersPaused": paused,
1274 "ContainersStopped": stopped,
1275 "Images": len(s.images),
1276 "Driver": "aufs",
1277 "DriverStatus": [][]string{},
1278 "SystemStatus": nil,
1279 "Plugins": map[string]interface{}{
1280 "Volume": []string{
1281 "local",
1282 },
1283 "Network": []string{
1284 "bridge",
1285 "null",
1286 "host",
1287 },
1288 "Authorization": nil,
1289 },
1290 "MemoryLimit": true,
1291 "SwapLimit": false,
1292 "CpuCfsPeriod": true,
1293 "CpuCfsQuota": true,
1294 "CPUShares": true,
1295 "CPUSet": true,
1296 "IPv4Forwarding": true,
1297 "BridgeNfIptables": true,
1298 "BridgeNfIp6tables": true,
1299 "Debug": false,
1300 "NFd": 79,
1301 "OomKillDisable": true,
1302 "NGoroutines": 101,
1303 "SystemTime": "2016-02-25T18:13:10.25870078Z",
1304 "ExecutionDriver": "native-0.2",
1305 "LoggingDriver": "json-file",
1306 "NEventsListener": 0,
1307 "KernelVersion": "3.13.0-77-generic",
1308 "OperatingSystem": "Ubuntu 14.04.3 LTS",
1309 "OSType": "linux",
1310 "Architecture": "x86_64",
1311 "IndexServerAddress": "https://index.docker.io/v1/",
1312 "RegistryConfig": map[string]interface{}{
1313 "InsecureRegistryCIDRs": []string{},
1314 "IndexConfigs": map[string]interface{}{},
1315 "Mirrors": nil,
1316 },
1317 "InitSha1": "e2042dbb0fcf49bb9da199186d9a5063cda92a01",
1318 "InitPath": "/usr/lib/docker/dockerinit",
1319 "NCPU": 1,
1320 "MemTotal": 2099204096,
1321 "DockerRootDir": "/var/lib/docker",
1322 "HttpProxy": "",
1323 "HttpsProxy": "",
1324 "NoProxy": "",
1325 "Name": "vagrant-ubuntu-trusty-64",
1326 "Labels": nil,
1327 "ExperimentalBuild": false,
1328 "ServerVersion": "1.10.1",
1329 "ClusterStore": "",
1330 "ClusterAdvertise": "",
1331 }
1332 w.WriteHeader(http.StatusOK)
1333 json.NewEncoder(w).Encode(envs)
1334}