Browse code

Add long-running client session endpoint

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>

Tonis Tiigi authored on 2017/05/16 04:59:15
Showing 17 changed files
... ...
@@ -27,9 +27,9 @@ type Backend struct {
27 27
 }
28 28
 
29 29
 // NewBackend creates a new build backend from components
30
-func NewBackend(components ImageComponent, builderBackend builder.Backend, idMappings *idtools.IDMappings) *Backend {
31
-	manager := dockerfile.NewBuildManager(builderBackend, idMappings)
32
-	return &Backend{imageComponent: components, manager: manager}
30
+func NewBackend(components ImageComponent, builderBackend builder.Backend, sg dockerfile.SessionGetter, idMappings *idtools.IDMappings) (*Backend, error) {
31
+	manager := dockerfile.NewBuildManager(builderBackend, sg, idMappings)
32
+	return &Backend{imageComponent: components, manager: manager}, nil
33 33
 }
34 34
 
35 35
 // Build builds an image from a Source
... ...
@@ -127,6 +127,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
127 127
 		}
128 128
 		options.CacheFrom = cacheFrom
129 129
 	}
130
+	options.SessionID = r.FormValue("session")
130 131
 
131 132
 	return options, nil
132 133
 }
133 134
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package session
1
+
2
+import (
3
+	"net/http"
4
+
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// Backend abstracts an session receiver from an http request.
9
+type Backend interface {
10
+	HandleHTTPRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error
11
+}
0 12
new file mode 100644
... ...
@@ -0,0 +1,29 @@
0
+package session
1
+
2
+import "github.com/docker/docker/api/server/router"
3
+
4
+// sessionRouter is a router to talk with the session controller
5
+type sessionRouter struct {
6
+	backend Backend
7
+	routes  []router.Route
8
+}
9
+
10
+// NewRouter initializes a new session router
11
+func NewRouter(b Backend) router.Router {
12
+	r := &sessionRouter{
13
+		backend: b,
14
+	}
15
+	r.initRoutes()
16
+	return r
17
+}
18
+
19
+// Routes returns the available routers to the session controller
20
+func (r *sessionRouter) Routes() []router.Route {
21
+	return r.routes
22
+}
23
+
24
+func (r *sessionRouter) initRoutes() {
25
+	r.routes = []router.Route{
26
+		router.Experimental(router.NewPostRoute("/session", r.startSession)),
27
+	}
28
+}
0 29
new file mode 100644
... ...
@@ -0,0 +1,16 @@
0
+package session
1
+
2
+import (
3
+	"net/http"
4
+
5
+	apierrors "github.com/docker/docker/api/errors"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+func (sr *sessionRouter) startSession(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
10
+	err := sr.backend.HandleHTTPRequest(ctx, w, r)
11
+	if err != nil {
12
+		return apierrors.NewBadRequestError(err)
13
+	}
14
+	return nil
15
+}
... ...
@@ -7,7 +7,7 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/api/types/container"
9 9
 	"github.com/docker/docker/api/types/filters"
10
-	"github.com/docker/go-units"
10
+	units "github.com/docker/go-units"
11 11
 )
12 12
 
13 13
 // CheckpointCreateOptions holds parameters to create a checkpoint from a container
... ...
@@ -178,6 +178,7 @@ type ImageBuildOptions struct {
178 178
 	SecurityOpt []string
179 179
 	ExtraHosts  []string // List of extra hosts
180 180
 	Target      string
181
+	SessionID   string
181 182
 
182 183
 	// TODO @jhowardmsft LCOW Support: This will require extending to include
183 184
 	// `Platform string`, but is ommited for now as it's hard-coded temporarily
... ...
@@ -16,6 +16,7 @@ import (
16 16
 	"github.com/docker/docker/builder/dockerfile/command"
17 17
 	"github.com/docker/docker/builder/dockerfile/parser"
18 18
 	"github.com/docker/docker/builder/remotecontext"
19
+	"github.com/docker/docker/client/session"
19 20
 	"github.com/docker/docker/pkg/archive"
20 21
 	"github.com/docker/docker/pkg/chrootarchive"
21 22
 	"github.com/docker/docker/pkg/idtools"
... ...
@@ -40,18 +41,25 @@ var validCommitCommands = map[string]bool{
40 40
 	"workdir":     true,
41 41
 }
42 42
 
43
+// SessionGetter is object used to get access to a session by uuid
44
+type SessionGetter interface {
45
+	Get(ctx context.Context, uuid string) (session.Caller, error)
46
+}
47
+
43 48
 // BuildManager is shared across all Builder objects
44 49
 type BuildManager struct {
45 50
 	archiver  *archive.Archiver
46 51
 	backend   builder.Backend
47 52
 	pathCache pathCache // TODO: make this persistent
53
+	sg        SessionGetter
48 54
 }
49 55
 
50 56
 // NewBuildManager creates a BuildManager
51
-func NewBuildManager(b builder.Backend, idMappings *idtools.IDMappings) *BuildManager {
57
+func NewBuildManager(b builder.Backend, sg SessionGetter, idMappings *idtools.IDMappings) *BuildManager {
52 58
 	return &BuildManager{
53 59
 		backend:   b,
54 60
 		pathCache: &syncmap.Map{},
61
+		sg:        sg,
55 62
 		archiver:  chrootarchive.NewArchiver(idMappings),
56 63
 	}
57 64
 }
... ...
@@ -84,6 +92,13 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (
84 84
 		}
85 85
 	}
86 86
 
87
+	ctx, cancel := context.WithCancel(ctx)
88
+	defer cancel()
89
+
90
+	if err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
91
+		return nil, err
92
+	}
93
+
87 94
 	builderOptions := builderOptions{
88 95
 		Options:        config.Options,
89 96
 		ProgressWriter: config.ProgressWriter,
... ...
@@ -96,6 +111,22 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (
96 96
 	return newBuilder(ctx, builderOptions).build(source, dockerfile)
97 97
 }
98 98
 
99
+func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) error {
100
+	if options.SessionID == "" || bm.sg == nil {
101
+		return nil
102
+	}
103
+	logrus.Debug("client is session enabled")
104
+	c, err := bm.sg.Get(ctx, options.SessionID)
105
+	if err != nil {
106
+		return err
107
+	}
108
+	go func() {
109
+		<-c.Context().Done()
110
+		cancel()
111
+	}()
112
+	return nil
113
+}
114
+
99 115
 // builderOptions are the dependencies required by the builder
100 116
 type builderOptions struct {
101 117
 	Options        *types.ImageBuildOptions
... ...
@@ -1,11 +1,9 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"bytes"
4
+	"bufio"
5 5
 	"crypto/tls"
6
-	"errors"
7 6
 	"fmt"
8
-	"io/ioutil"
9 7
 	"net"
10 8
 	"net/http"
11 9
 	"net/http/httputil"
... ...
@@ -16,6 +14,7 @@ import (
16 16
 	"github.com/docker/docker/api/types"
17 17
 	"github.com/docker/docker/pkg/tlsconfig"
18 18
 	"github.com/docker/go-connections/sockets"
19
+	"github.com/pkg/errors"
19 20
 	"golang.org/x/net/context"
20 21
 )
21 22
 
... ...
@@ -48,49 +47,12 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
48 48
 	}
49 49
 	req = cli.addHeaders(req, headers)
50 50
 
51
-	req.Host = cli.addr
52
-	req.Header.Set("Connection", "Upgrade")
53
-	req.Header.Set("Upgrade", "tcp")
54
-
55
-	conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
51
+	conn, err := cli.setupHijackConn(req, "tcp")
56 52
 	if err != nil {
57
-		if strings.Contains(err.Error(), "connection refused") {
58
-			return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
59
-		}
60 53
 		return types.HijackedResponse{}, err
61 54
 	}
62 55
 
63
-	// When we set up a TCP connection for hijack, there could be long periods
64
-	// of inactivity (a long running command with no output) that in certain
65
-	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
66
-	// state. Setting TCP KeepAlive on the socket connection will prohibit
67
-	// ECONNTIMEOUT unless the socket connection truly is broken
68
-	if tcpConn, ok := conn.(*net.TCPConn); ok {
69
-		tcpConn.SetKeepAlive(true)
70
-		tcpConn.SetKeepAlivePeriod(30 * time.Second)
71
-	}
72
-
73
-	clientconn := httputil.NewClientConn(conn, nil)
74
-	defer clientconn.Close()
75
-
76
-	// Server hijacks the connection, error 'connection closed' expected
77
-	resp, err := clientconn.Do(req)
78
-	if err != nil {
79
-		return types.HijackedResponse{}, err
80
-	}
81
-
82
-	defer resp.Body.Close()
83
-	switch resp.StatusCode {
84
-	case http.StatusOK, http.StatusSwitchingProtocols:
85
-		rwc, br := clientconn.Hijack()
86
-		return types.HijackedResponse{Conn: rwc, Reader: br}, err
87
-	}
88
-
89
-	errbody, err := ioutil.ReadAll(resp.Body)
90
-	if err != nil {
91
-		return types.HijackedResponse{}, err
92
-	}
93
-	return types.HijackedResponse{}, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(errbody))
56
+	return types.HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn)}, err
94 57
 }
95 58
 
96 59
 func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
... ...
@@ -189,3 +151,56 @@ func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
189 189
 	}
190 190
 	return net.Dial(proto, addr)
191 191
 }
192
+
193
+func (cli *Client) setupHijackConn(req *http.Request, proto string) (net.Conn, error) {
194
+	req.Host = cli.addr
195
+	req.Header.Set("Connection", "Upgrade")
196
+	req.Header.Set("Upgrade", proto)
197
+
198
+	conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
199
+	if err != nil {
200
+		return nil, errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
201
+	}
202
+
203
+	// When we set up a TCP connection for hijack, there could be long periods
204
+	// of inactivity (a long running command with no output) that in certain
205
+	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
206
+	// state. Setting TCP KeepAlive on the socket connection will prohibit
207
+	// ECONNTIMEOUT unless the socket connection truly is broken
208
+	if tcpConn, ok := conn.(*net.TCPConn); ok {
209
+		tcpConn.SetKeepAlive(true)
210
+		tcpConn.SetKeepAlivePeriod(30 * time.Second)
211
+	}
212
+
213
+	clientconn := httputil.NewClientConn(conn, nil)
214
+	defer clientconn.Close()
215
+
216
+	// Server hijacks the connection, error 'connection closed' expected
217
+	resp, err := clientconn.Do(req)
218
+	if err != nil {
219
+		return nil, err
220
+	}
221
+	if resp.StatusCode != http.StatusSwitchingProtocols {
222
+		resp.Body.Close()
223
+		return nil, fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
224
+	}
225
+
226
+	c, br := clientconn.Hijack()
227
+	if br.Buffered() > 0 {
228
+		// If there is buffered content, wrap the connection
229
+		c = &hijackedConn{c, br}
230
+	} else {
231
+		br.Reset(nil)
232
+	}
233
+
234
+	return c, nil
235
+}
236
+
237
+type hijackedConn struct {
238
+	net.Conn
239
+	r *bufio.Reader
240
+}
241
+
242
+func (c *hijackedConn) Read(b []byte) (int, error) {
243
+	return c.r.Read(b)
244
+}
... ...
@@ -120,6 +120,9 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur
120 120
 		return query, err
121 121
 	}
122 122
 	query.Set("cachefrom", string(cacheFromJSON))
123
+	if options.SessionID != "" {
124
+		query.Set("session", options.SessionID)
125
+	}
123 126
 
124 127
 	return query, nil
125 128
 }
... ...
@@ -2,6 +2,7 @@ package client
2 2
 
3 3
 import (
4 4
 	"io"
5
+	"net"
5 6
 	"time"
6 7
 
7 8
 	"github.com/docker/docker/api/types"
... ...
@@ -35,6 +36,7 @@ type CommonAPIClient interface {
35 35
 	ServerVersion(ctx context.Context) (types.Version, error)
36 36
 	NegotiateAPIVersion(ctx context.Context)
37 37
 	NegotiateAPIVersionPing(types.Ping)
38
+	DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error)
38 39
 }
39 40
 
40 41
 // ContainerAPIClient defines API client methods for the containers
41 42
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+package client
1
+
2
+import (
3
+	"net"
4
+	"net/http"
5
+
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// DialSession returns a connection that can be used communication with daemon
10
+func (cli *Client) DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
11
+	req, err := http.NewRequest("POST", "/session", nil)
12
+	if err != nil {
13
+		return nil, err
14
+	}
15
+	req = cli.addHeaders(req, meta)
16
+
17
+	return cli.setupHijackConn(req, proto)
18
+}
0 19
new file mode 100644
... ...
@@ -0,0 +1,62 @@
0
+package session
1
+
2
+import (
3
+	"net"
4
+	"time"
5
+
6
+	"github.com/Sirupsen/logrus"
7
+	"github.com/pkg/errors"
8
+	"golang.org/x/net/context"
9
+	"golang.org/x/net/http2"
10
+	"google.golang.org/grpc"
11
+	"google.golang.org/grpc/health/grpc_health_v1"
12
+)
13
+
14
+func serve(ctx context.Context, grpcServer *grpc.Server, conn net.Conn) {
15
+	go func() {
16
+		<-ctx.Done()
17
+		conn.Close()
18
+	}()
19
+	logrus.Debugf("serving grpc connection")
20
+	(&http2.Server{}).ServeConn(conn, &http2.ServeConnOpts{Handler: grpcServer})
21
+}
22
+
23
+func grpcClientConn(ctx context.Context, conn net.Conn) (context.Context, *grpc.ClientConn, error) {
24
+	dialOpt := grpc.WithDialer(func(addr string, d time.Duration) (net.Conn, error) {
25
+		return conn, nil
26
+	})
27
+
28
+	cc, err := grpc.DialContext(ctx, "", dialOpt, grpc.WithInsecure())
29
+	if err != nil {
30
+		return nil, nil, errors.Wrap(err, "failed to create grpc client")
31
+	}
32
+
33
+	ctx, cancel := context.WithCancel(ctx)
34
+	go monitorHealth(ctx, cc, cancel)
35
+
36
+	return ctx, cc, nil
37
+}
38
+
39
+func monitorHealth(ctx context.Context, cc *grpc.ClientConn, cancelConn func()) {
40
+	defer cancelConn()
41
+	defer cc.Close()
42
+
43
+	ticker := time.NewTicker(500 * time.Millisecond)
44
+	defer ticker.Stop()
45
+	healthClient := grpc_health_v1.NewHealthClient(cc)
46
+
47
+	for {
48
+		select {
49
+		case <-ctx.Done():
50
+			return
51
+		case <-ticker.C:
52
+			<-ticker.C
53
+			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
54
+			_, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
55
+			cancel()
56
+			if err != nil {
57
+				return
58
+			}
59
+		}
60
+	}
61
+}
0 62
new file mode 100644
... ...
@@ -0,0 +1,187 @@
0
+package session
1
+
2
+import (
3
+	"net/http"
4
+	"strings"
5
+	"sync"
6
+
7
+	"github.com/pkg/errors"
8
+	"golang.org/x/net/context"
9
+	"google.golang.org/grpc"
10
+)
11
+
12
+// Caller can invoke requests on the session
13
+type Caller interface {
14
+	Context() context.Context
15
+	Supports(method string) bool
16
+	Conn() *grpc.ClientConn
17
+	Name() string
18
+	SharedKey() string
19
+}
20
+
21
+type client struct {
22
+	Session
23
+	cc        *grpc.ClientConn
24
+	supported map[string]struct{}
25
+}
26
+
27
+// Manager is a controller for accessing currently active sessions
28
+type Manager struct {
29
+	sessions        map[string]*client
30
+	mu              sync.Mutex
31
+	updateCondition *sync.Cond
32
+}
33
+
34
+// NewManager returns a new Manager
35
+func NewManager() (*Manager, error) {
36
+	sm := &Manager{
37
+		sessions: make(map[string]*client),
38
+	}
39
+	sm.updateCondition = sync.NewCond(&sm.mu)
40
+	return sm, nil
41
+}
42
+
43
+// HandleHTTPRequest handles an incoming HTTP request
44
+func (sm *Manager) HandleHTTPRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
45
+	hijacker, ok := w.(http.Hijacker)
46
+	if !ok {
47
+		return errors.New("handler does not support hijack")
48
+	}
49
+
50
+	uuid := r.Header.Get(headerSessionUUID)
51
+	name := r.Header.Get(headerSessionName)
52
+	sharedKey := r.Header.Get(headerSessionSharedKey)
53
+
54
+	proto := r.Header.Get("Upgrade")
55
+
56
+	sm.mu.Lock()
57
+	if _, ok := sm.sessions[uuid]; ok {
58
+		sm.mu.Unlock()
59
+		return errors.Errorf("session %s already exists", uuid)
60
+	}
61
+
62
+	if proto == "" {
63
+		sm.mu.Unlock()
64
+		return errors.New("no upgrade proto in request")
65
+	}
66
+
67
+	if proto != "h2c" {
68
+		sm.mu.Unlock()
69
+		return errors.Errorf("protocol %s not supported", proto)
70
+	}
71
+
72
+	conn, _, err := hijacker.Hijack()
73
+	if err != nil {
74
+		sm.mu.Unlock()
75
+		return errors.Wrap(err, "failed to hijack connection")
76
+	}
77
+
78
+	resp := &http.Response{
79
+		StatusCode: http.StatusSwitchingProtocols,
80
+		ProtoMajor: 1,
81
+		ProtoMinor: 1,
82
+		Header:     http.Header{},
83
+	}
84
+	resp.Header.Set("Connection", "Upgrade")
85
+	resp.Header.Set("Upgrade", proto)
86
+
87
+	// set raw mode
88
+	conn.Write([]byte{})
89
+	resp.Write(conn)
90
+
91
+	ctx, cancel := context.WithCancel(ctx)
92
+	defer cancel()
93
+
94
+	ctx, cc, err := grpcClientConn(ctx, conn)
95
+	if err != nil {
96
+		sm.mu.Unlock()
97
+		return err
98
+	}
99
+
100
+	c := &client{
101
+		Session: Session{
102
+			uuid:      uuid,
103
+			name:      name,
104
+			sharedKey: sharedKey,
105
+			ctx:       ctx,
106
+			cancelCtx: cancel,
107
+			done:      make(chan struct{}),
108
+		},
109
+		cc:        cc,
110
+		supported: make(map[string]struct{}),
111
+	}
112
+
113
+	for _, m := range r.Header[headerSessionMethod] {
114
+		c.supported[strings.ToLower(m)] = struct{}{}
115
+	}
116
+	sm.sessions[uuid] = c
117
+	sm.updateCondition.Broadcast()
118
+	sm.mu.Unlock()
119
+
120
+	defer func() {
121
+		sm.mu.Lock()
122
+		delete(sm.sessions, uuid)
123
+		sm.mu.Unlock()
124
+	}()
125
+
126
+	<-c.ctx.Done()
127
+	conn.Close()
128
+	close(c.done)
129
+
130
+	return nil
131
+}
132
+
133
+// Get returns a session by UUID
134
+func (sm *Manager) Get(ctx context.Context, uuid string) (Caller, error) {
135
+	ctx, cancel := context.WithCancel(ctx)
136
+	defer cancel()
137
+
138
+	go func() {
139
+		select {
140
+		case <-ctx.Done():
141
+			sm.updateCondition.Broadcast()
142
+		}
143
+	}()
144
+
145
+	var c *client
146
+
147
+	sm.mu.Lock()
148
+	for {
149
+		select {
150
+		case <-ctx.Done():
151
+			sm.mu.Unlock()
152
+			return nil, errors.Wrapf(ctx.Err(), "no active session for %s", uuid)
153
+		default:
154
+		}
155
+		var ok bool
156
+		c, ok = sm.sessions[uuid]
157
+		if !ok || c.closed() {
158
+			sm.updateCondition.Wait()
159
+			continue
160
+		}
161
+		sm.mu.Unlock()
162
+		break
163
+	}
164
+
165
+	return c, nil
166
+}
167
+
168
+func (c *client) Context() context.Context {
169
+	return c.context()
170
+}
171
+
172
+func (c *client) Name() string {
173
+	return c.name
174
+}
175
+
176
+func (c *client) SharedKey() string {
177
+	return c.sharedKey
178
+}
179
+
180
+func (c *client) Supports(url string) bool {
181
+	_, ok := c.supported[strings.ToLower(url)]
182
+	return ok
183
+}
184
+func (c *client) Conn() *grpc.ClientConn {
185
+	return c.cc
186
+}
0 187
new file mode 100644
... ...
@@ -0,0 +1,117 @@
0
+package session
1
+
2
+import (
3
+	"net"
4
+
5
+	"github.com/docker/docker/pkg/stringid"
6
+	"github.com/pkg/errors"
7
+	"golang.org/x/net/context"
8
+	"google.golang.org/grpc"
9
+	"google.golang.org/grpc/health"
10
+	"google.golang.org/grpc/health/grpc_health_v1"
11
+)
12
+
13
+const (
14
+	headerSessionUUID      = "X-Docker-Expose-Session-Uuid"
15
+	headerSessionName      = "X-Docker-Expose-Session-Name"
16
+	headerSessionSharedKey = "X-Docker-Expose-Session-Sharedkey"
17
+	headerSessionMethod    = "X-Docker-Expose-Session-Grpc-Method"
18
+)
19
+
20
+// Dialer returns a connection that can be used by the session
21
+type Dialer func(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error)
22
+
23
+// Attachable defines a feature that can be expsed on a session
24
+type Attachable interface {
25
+	Register(*grpc.Server)
26
+}
27
+
28
+// Session is a long running connection between client and a daemon
29
+type Session struct {
30
+	uuid       string
31
+	name       string
32
+	sharedKey  string
33
+	ctx        context.Context
34
+	cancelCtx  func()
35
+	done       chan struct{}
36
+	grpcServer *grpc.Server
37
+}
38
+
39
+// NewSession returns a new long running session
40
+func NewSession(name, sharedKey string) (*Session, error) {
41
+	uuid := stringid.GenerateRandomID()
42
+	s := &Session{
43
+		uuid:       uuid,
44
+		name:       name,
45
+		sharedKey:  sharedKey,
46
+		grpcServer: grpc.NewServer(),
47
+	}
48
+
49
+	grpc_health_v1.RegisterHealthServer(s.grpcServer, health.NewServer())
50
+
51
+	return s, nil
52
+}
53
+
54
+// Allow enable a given service to be reachable through the grpc session
55
+func (s *Session) Allow(a Attachable) {
56
+	a.Register(s.grpcServer)
57
+}
58
+
59
+// UUID returns unique identifier for the session
60
+func (s *Session) UUID() string {
61
+	return s.uuid
62
+}
63
+
64
+// Run activates the session
65
+func (s *Session) Run(ctx context.Context, dialer Dialer) error {
66
+	ctx, cancel := context.WithCancel(ctx)
67
+	s.cancelCtx = cancel
68
+	s.done = make(chan struct{})
69
+
70
+	defer cancel()
71
+	defer close(s.done)
72
+
73
+	meta := make(map[string][]string)
74
+	meta[headerSessionUUID] = []string{s.uuid}
75
+	meta[headerSessionName] = []string{s.name}
76
+	meta[headerSessionSharedKey] = []string{s.sharedKey}
77
+
78
+	for name, svc := range s.grpcServer.GetServiceInfo() {
79
+		for _, method := range svc.Methods {
80
+			meta[headerSessionMethod] = append(meta[headerSessionMethod], MethodURL(name, method.Name))
81
+		}
82
+	}
83
+	conn, err := dialer(ctx, "h2c", meta)
84
+	if err != nil {
85
+		return errors.Wrap(err, "failed to dial gRPC")
86
+	}
87
+	serve(ctx, s.grpcServer, conn)
88
+	return nil
89
+}
90
+
91
+// Close closes the session
92
+func (s *Session) Close() error {
93
+	if s.cancelCtx != nil && s.done != nil {
94
+		s.cancelCtx()
95
+		<-s.done
96
+	}
97
+	return nil
98
+}
99
+
100
+func (s *Session) context() context.Context {
101
+	return s.ctx
102
+}
103
+
104
+func (s *Session) closed() bool {
105
+	select {
106
+	case <-s.context().Done():
107
+		return true
108
+	default:
109
+		return false
110
+	}
111
+}
112
+
113
+// MethodURL returns a gRPC method URL for service and method name
114
+func MethodURL(s, m string) string {
115
+	return "/" + s + "/" + m
116
+}
... ...
@@ -23,10 +23,12 @@ import (
23 23
 	"github.com/docker/docker/api/server/router/image"
24 24
 	"github.com/docker/docker/api/server/router/network"
25 25
 	pluginrouter "github.com/docker/docker/api/server/router/plugin"
26
+	sessionrouter "github.com/docker/docker/api/server/router/session"
26 27
 	swarmrouter "github.com/docker/docker/api/server/router/swarm"
27 28
 	systemrouter "github.com/docker/docker/api/server/router/system"
28 29
 	"github.com/docker/docker/api/server/router/volume"
29 30
 	"github.com/docker/docker/cli/debug"
31
+	"github.com/docker/docker/client/session"
30 32
 	"github.com/docker/docker/daemon"
31 33
 	"github.com/docker/docker/daemon/cluster"
32 34
 	"github.com/docker/docker/daemon/config"
... ...
@@ -46,6 +48,7 @@ import (
46 46
 	"github.com/docker/docker/runconfig"
47 47
 	"github.com/docker/go-connections/tlsconfig"
48 48
 	swarmapi "github.com/docker/swarmkit/api"
49
+	"github.com/pkg/errors"
49 50
 	"github.com/spf13/pflag"
50 51
 )
51 52
 
... ...
@@ -215,6 +218,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
215 215
 		logrus.Warnln("LCOW support is enabled - this feature is incomplete")
216 216
 	}
217 217
 
218
+	sm, err := session.NewManager()
219
+	if err != nil {
220
+		return errors.Wrap(err, "failed to create sessionmanager")
221
+	}
222
+
218 223
 	d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote, pluginStore)
219 224
 	if err != nil {
220 225
 		return fmt.Errorf("Error starting daemon: %v", err)
... ...
@@ -260,6 +268,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
260 260
 		logrus.Fatalf("Error starting cluster component: %v", err)
261 261
 	}
262 262
 
263
+	bb, err := buildbackend.NewBackend(d, d, sm, d.IDMappings())
264
+	if err != nil {
265
+		return errors.Wrap(err, "failed to create buildmanager")
266
+	}
267
+
263 268
 	// Restart all autostart containers which has a swarm endpoint
264 269
 	// and is not yet running now that we have successfully
265 270
 	// initialized the cluster.
... ...
@@ -269,7 +282,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
269 269
 
270 270
 	cli.d = d
271 271
 
272
-	initRouter(api, d, c)
272
+	initRouter(api, d, c, sm, bb)
273 273
 
274 274
 	// process cluster change notifications
275 275
 	watchCtx, cancel := context.WithCancel(context.Background())
... ...
@@ -442,7 +455,7 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
442 442
 	return conf, nil
443 443
 }
444 444
 
445
-func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
445
+func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster, sm *session.Manager, bb *buildbackend.Backend) {
446 446
 	decoder := runconfig.ContainerDecoder{}
447 447
 
448 448
 	routers := []router.Router{
... ...
@@ -452,7 +465,8 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
452 452
 		image.NewRouter(d, decoder),
453 453
 		systemrouter.NewRouter(d, c),
454 454
 		volume.NewRouter(d),
455
-		build.NewRouter(buildbackend.NewBackend(d, d, d.IDMappings()), d),
455
+		build.NewRouter(bb, d),
456
+		sessionrouter.NewRouter(sm),
456 457
 		swarmrouter.NewRouter(c),
457 458
 		pluginrouter.NewRouter(d.PluginManager()),
458 459
 		distributionrouter.NewRouter(d),
459 460
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package main
1
+
2
+import (
3
+	"net/http"
4
+
5
+	"github.com/docker/docker/integration-cli/checker"
6
+	"github.com/docker/docker/integration-cli/request"
7
+	"github.com/docker/docker/pkg/testutil"
8
+	"github.com/go-check/check"
9
+)
10
+
11
+func (s *DockerSuite) TestSessionCreate(c *check.C) {
12
+	testRequires(c, ExperimentalDaemon)
13
+
14
+	res, body, err := request.Post("/session", func(r *http.Request) error {
15
+		r.Header.Set("X-Docker-Expose-Session-Uuid", "testsessioncreate") // so we don't block default name if something else is using it
16
+		r.Header.Set("Upgrade", "h2c")
17
+		return nil
18
+	})
19
+	c.Assert(err, checker.IsNil)
20
+	c.Assert(res.StatusCode, checker.Equals, http.StatusSwitchingProtocols)
21
+	c.Assert(res.Header.Get("Upgrade"), checker.Equals, "h2c")
22
+	c.Assert(body.Close(), checker.IsNil)
23
+}
24
+
25
+func (s *DockerSuite) TestSessionCreateWithBadUpgrade(c *check.C) {
26
+	testRequires(c, ExperimentalDaemon)
27
+
28
+	res, body, err := request.Post("/session")
29
+	c.Assert(err, checker.IsNil)
30
+	c.Assert(res.StatusCode, checker.Equals, http.StatusBadRequest)
31
+	buf, err := testutil.ReadBody(body)
32
+	c.Assert(err, checker.IsNil)
33
+
34
+	out := string(buf)
35
+	c.Assert(out, checker.Contains, "no upgrade")
36
+
37
+	res, body, err = request.Post("/session", func(r *http.Request) error {
38
+		r.Header.Set("Upgrade", "foo")
39
+		return nil
40
+	})
41
+	c.Assert(err, checker.IsNil)
42
+	c.Assert(res.StatusCode, checker.Equals, http.StatusBadRequest)
43
+	buf, err = testutil.ReadBody(body)
44
+	c.Assert(err, checker.IsNil)
45
+
46
+	out = string(buf)
47
+	c.Assert(out, checker.Contains, "not supported")
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+// Package health provides some utility functions to health-check a server. The implementation
1
+// is based on protobuf. Users need to write their own implementations if other IDLs are used.
2
+package health
3
+
4
+import (
5
+	"sync"
6
+
7
+	"golang.org/x/net/context"
8
+	"google.golang.org/grpc"
9
+	"google.golang.org/grpc/codes"
10
+	healthpb "google.golang.org/grpc/health/grpc_health_v1"
11
+)
12
+
13
+// Server implements `service Health`.
14
+type Server struct {
15
+	mu sync.Mutex
16
+	// statusMap stores the serving status of the services this Server monitors.
17
+	statusMap map[string]healthpb.HealthCheckResponse_ServingStatus
18
+}
19
+
20
+// NewServer returns a new Server.
21
+func NewServer() *Server {
22
+	return &Server{
23
+		statusMap: make(map[string]healthpb.HealthCheckResponse_ServingStatus),
24
+	}
25
+}
26
+
27
+// Check implements `service Health`.
28
+func (s *Server) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
29
+	s.mu.Lock()
30
+	defer s.mu.Unlock()
31
+	if in.Service == "" {
32
+		// check the server overall health status.
33
+		return &healthpb.HealthCheckResponse{
34
+			Status: healthpb.HealthCheckResponse_SERVING,
35
+		}, nil
36
+	}
37
+	if status, ok := s.statusMap[in.Service]; ok {
38
+		return &healthpb.HealthCheckResponse{
39
+			Status: status,
40
+		}, nil
41
+	}
42
+	return nil, grpc.Errorf(codes.NotFound, "unknown service")
43
+}
44
+
45
+// SetServingStatus is called when need to reset the serving status of a service
46
+// or insert a new service entry into the statusMap.
47
+func (s *Server) SetServingStatus(service string, status healthpb.HealthCheckResponse_ServingStatus) {
48
+	s.mu.Lock()
49
+	s.statusMap[service] = status
50
+	s.mu.Unlock()
51
+}