Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
| ... | ... |
@@ -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 |
| 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 |
+} |