Skip to content

Commit ec7b623

Browse files
committed
Add long-running client session endpoint
Signed-off-by: Tonis Tiigi <[email protected]>
1 parent f88626b commit ec7b623

File tree

17 files changed

+660
-50
lines changed

17 files changed

+660
-50
lines changed

api/server/backend/build/backend.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ type Backend struct {
2727
}
2828

2929
// 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
3333
}
3434

3535
// Build builds an image from a Source

api/server/router/build/build_routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
127127
}
128128
options.CacheFrom = cacheFrom
129129
}
130+
options.SessionID = r.FormValue("session")
130131

131132
return options, nil
132133
}

api/server/router/session/backend.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package session
2+
3+
import (
4+
"net/http"
5+
6+
"golang.org/x/net/context"
7+
)
8+
9+
// Backend abstracts an session receiver from an http request.
10+
type Backend interface {
11+
HandleHTTPRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error
12+
}

api/server/router/session/session.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package session
2+
3+
import "github.com/docker/docker/api/server/router"
4+
5+
// sessionRouter is a router to talk with the session controller
6+
type sessionRouter struct {
7+
backend Backend
8+
routes []router.Route
9+
}
10+
11+
// NewRouter initializes a new session router
12+
func NewRouter(b Backend) router.Router {
13+
r := &sessionRouter{
14+
backend: b,
15+
}
16+
r.initRoutes()
17+
return r
18+
}
19+
20+
// Routes returns the available routers to the session controller
21+
func (r *sessionRouter) Routes() []router.Route {
22+
return r.routes
23+
}
24+
25+
func (r *sessionRouter) initRoutes() {
26+
r.routes = []router.Route{
27+
router.Experimental(router.NewPostRoute("/session", r.startSession)),
28+
}
29+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package session
2+
3+
import (
4+
"net/http"
5+
6+
apierrors "github.com/docker/docker/api/errors"
7+
"golang.org/x/net/context"
8+
)
9+
10+
func (sr *sessionRouter) startSession(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
11+
err := sr.backend.HandleHTTPRequest(ctx, w, r)
12+
if err != nil {
13+
return apierrors.NewBadRequestError(err)
14+
}
15+
return nil
16+
}

api/types/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
"github.com/docker/docker/api/types/container"
99
"github.com/docker/docker/api/types/filters"
10-
"github.com/docker/go-units"
10+
units "github.com/docker/go-units"
1111
)
1212

1313
// CheckpointCreateOptions holds parameters to create a checkpoint from a container
@@ -178,6 +178,7 @@ type ImageBuildOptions struct {
178178
SecurityOpt []string
179179
ExtraHosts []string // List of extra hosts
180180
Target string
181+
SessionID string
181182

182183
// TODO @jhowardmsft LCOW Support: This will require extending to include
183184
// `Platform string`, but is ommited for now as it's hard-coded temporarily

builder/dockerfile/builder.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/docker/docker/builder/dockerfile/command"
1717
"github.com/docker/docker/builder/dockerfile/parser"
1818
"github.com/docker/docker/builder/remotecontext"
19+
"github.com/docker/docker/client/session"
1920
"github.com/docker/docker/pkg/archive"
2021
"github.com/docker/docker/pkg/chrootarchive"
2122
"github.com/docker/docker/pkg/idtools"
@@ -40,18 +41,25 @@ var validCommitCommands = map[string]bool{
4041
"workdir": true,
4142
}
4243

44+
// SessionGetter is object used to get access to a session by uuid
45+
type SessionGetter interface {
46+
Get(ctx context.Context, uuid string) (session.Caller, error)
47+
}
48+
4349
// BuildManager is shared across all Builder objects
4450
type BuildManager struct {
4551
archiver *archive.Archiver
4652
backend builder.Backend
4753
pathCache pathCache // TODO: make this persistent
54+
sg SessionGetter
4855
}
4956

5057
// NewBuildManager creates a BuildManager
51-
func NewBuildManager(b builder.Backend, idMappings *idtools.IDMappings) *BuildManager {
58+
func NewBuildManager(b builder.Backend, sg SessionGetter, idMappings *idtools.IDMappings) *BuildManager {
5259
return &BuildManager{
5360
backend: b,
5461
pathCache: &syncmap.Map{},
62+
sg: sg,
5563
archiver: chrootarchive.NewArchiver(idMappings),
5664
}
5765
}
@@ -84,6 +92,13 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (
8492
}
8593
}
8694

95+
ctx, cancel := context.WithCancel(ctx)
96+
defer cancel()
97+
98+
if err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
99+
return nil, err
100+
}
101+
87102
builderOptions := builderOptions{
88103
Options: config.Options,
89104
ProgressWriter: config.ProgressWriter,
@@ -96,6 +111,22 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (
96111
return newBuilder(ctx, builderOptions).build(source, dockerfile)
97112
}
98113

114+
func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) error {
115+
if options.SessionID == "" || bm.sg == nil {
116+
return nil
117+
}
118+
logrus.Debug("client is session enabled")
119+
c, err := bm.sg.Get(ctx, options.SessionID)
120+
if err != nil {
121+
return err
122+
}
123+
go func() {
124+
<-c.Context().Done()
125+
cancel()
126+
}()
127+
return nil
128+
}
129+
99130
// builderOptions are the dependencies required by the builder
100131
type builderOptions struct {
101132
Options *types.ImageBuildOptions

client/hijack.go

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package client
22

33
import (
4-
"bytes"
4+
"bufio"
55
"crypto/tls"
6-
"errors"
76
"fmt"
8-
"io/ioutil"
97
"net"
108
"net/http"
119
"net/http/httputil"
@@ -16,6 +14,7 @@ import (
1614
"github.com/docker/docker/api/types"
1715
"github.com/docker/docker/pkg/tlsconfig"
1816
"github.com/docker/go-connections/sockets"
17+
"github.com/pkg/errors"
1918
"golang.org/x/net/context"
2019
)
2120

@@ -48,49 +47,12 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
4847
}
4948
req = cli.addHeaders(req, headers)
5049

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))
50+
conn, err := cli.setupHijackConn(req, "tcp")
5651
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-
}
6052
return types.HijackedResponse{}, err
6153
}
6254

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))
55+
return types.HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn)}, err
9456
}
9557

9658
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) {
189151
}
190152
return net.Dial(proto, addr)
191153
}
154+
155+
func (cli *Client) setupHijackConn(req *http.Request, proto string) (net.Conn, error) {
156+
req.Host = cli.addr
157+
req.Header.Set("Connection", "Upgrade")
158+
req.Header.Set("Upgrade", proto)
159+
160+
conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
161+
if err != nil {
162+
return nil, errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
163+
}
164+
165+
// When we set up a TCP connection for hijack, there could be long periods
166+
// of inactivity (a long running command with no output) that in certain
167+
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
168+
// state. Setting TCP KeepAlive on the socket connection will prohibit
169+
// ECONNTIMEOUT unless the socket connection truly is broken
170+
if tcpConn, ok := conn.(*net.TCPConn); ok {
171+
tcpConn.SetKeepAlive(true)
172+
tcpConn.SetKeepAlivePeriod(30 * time.Second)
173+
}
174+
175+
clientconn := httputil.NewClientConn(conn, nil)
176+
defer clientconn.Close()
177+
178+
// Server hijacks the connection, error 'connection closed' expected
179+
resp, err := clientconn.Do(req)
180+
if err != nil {
181+
return nil, err
182+
}
183+
if resp.StatusCode != http.StatusSwitchingProtocols {
184+
resp.Body.Close()
185+
return nil, fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
186+
}
187+
188+
c, br := clientconn.Hijack()
189+
if br.Buffered() > 0 {
190+
// If there is buffered content, wrap the connection
191+
c = &hijackedConn{c, br}
192+
} else {
193+
br.Reset(nil)
194+
}
195+
196+
return c, nil
197+
}
198+
199+
type hijackedConn struct {
200+
net.Conn
201+
r *bufio.Reader
202+
}
203+
204+
func (c *hijackedConn) Read(b []byte) (int, error) {
205+
return c.r.Read(b)
206+
}

client/image_build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur
120120
return query, err
121121
}
122122
query.Set("cachefrom", string(cacheFromJSON))
123+
if options.SessionID != "" {
124+
query.Set("session", options.SessionID)
125+
}
123126

124127
return query, nil
125128
}

client/interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"io"
5+
"net"
56
"time"
67

78
"github.com/docker/docker/api/types"
@@ -35,6 +36,7 @@ type CommonAPIClient interface {
3536
ServerVersion(ctx context.Context) (types.Version, error)
3637
NegotiateAPIVersion(ctx context.Context)
3738
NegotiateAPIVersionPing(types.Ping)
39+
DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error)
3840
}
3941

4042
// ContainerAPIClient defines API client methods for the containers

client/session.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package client
2+
3+
import (
4+
"net"
5+
"net/http"
6+
7+
"golang.org/x/net/context"
8+
)
9+
10+
// DialSession returns a connection that can be used communication with daemon
11+
func (cli *Client) DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
12+
req, err := http.NewRequest("POST", "/session", nil)
13+
if err != nil {
14+
return nil, err
15+
}
16+
req = cli.addHeaders(req, meta)
17+
18+
return cli.setupHijackConn(req, proto)
19+
}

0 commit comments

Comments
 (0)