github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/worker/gclient/connection/connection_test.go (about)

     1  package connection_test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"strings"
    16  	"time"
    17  
    18  	"code.cloudfoundry.org/lager/lagertest"
    19  	. "github.com/pf-qiu/concourse/v6/atc/worker/gclient/connection"
    20  	"github.com/pf-qiu/concourse/v6/atc/worker/gclient/connection/connectionfakes"
    21  	. "github.com/onsi/ginkgo"
    22  	. "github.com/onsi/gomega"
    23  	"github.com/onsi/gomega/gbytes"
    24  	"github.com/onsi/gomega/ghttp"
    25  	"github.com/tedsuo/rata"
    26  
    27  	"code.cloudfoundry.org/garden"
    28  	"code.cloudfoundry.org/garden/transport"
    29  )
    30  
    31  var _ = Describe("Connection", func() {
    32  	var (
    33  		connection     Connection
    34  		resourceLimits garden.ResourceLimits
    35  		server         *ghttp.Server
    36  		hijacker       HijackStreamer
    37  		network        string
    38  		address        string
    39  	)
    40  
    41  	BeforeEach(func() {
    42  		server = ghttp.NewServer()
    43  		network = "tcp"
    44  		address = server.HTTPTestServer.Listener.Addr().String()
    45  		hijacker = NewHijackStreamer(network, address)
    46  	})
    47  
    48  	JustBeforeEach(func() {
    49  		connection = NewWithHijacker(hijacker, lagertest.NewTestLogger("test-connection"))
    50  	})
    51  
    52  	BeforeEach(func() {
    53  		rlimits := &garden.ResourceLimits{
    54  			As:         uint64ptr(1),
    55  			Core:       uint64ptr(2),
    56  			Cpu:        uint64ptr(4),
    57  			Data:       uint64ptr(5),
    58  			Fsize:      uint64ptr(6),
    59  			Locks:      uint64ptr(7),
    60  			Memlock:    uint64ptr(8),
    61  			Msgqueue:   uint64ptr(9),
    62  			Nice:       uint64ptr(10),
    63  			Nofile:     uint64ptr(11),
    64  			Nproc:      uint64ptr(12),
    65  			Rss:        uint64ptr(13),
    66  			Rtprio:     uint64ptr(14),
    67  			Sigpending: uint64ptr(15),
    68  			Stack:      uint64ptr(16),
    69  		}
    70  
    71  		resourceLimits = garden.ResourceLimits{
    72  			As:         rlimits.As,
    73  			Core:       rlimits.Core,
    74  			Cpu:        rlimits.Cpu,
    75  			Data:       rlimits.Data,
    76  			Fsize:      rlimits.Fsize,
    77  			Locks:      rlimits.Locks,
    78  			Memlock:    rlimits.Memlock,
    79  			Msgqueue:   rlimits.Msgqueue,
    80  			Nice:       rlimits.Nice,
    81  			Nofile:     rlimits.Nofile,
    82  			Nproc:      rlimits.Nproc,
    83  			Rss:        rlimits.Rss,
    84  			Rtprio:     rlimits.Rtprio,
    85  			Sigpending: rlimits.Sigpending,
    86  			Stack:      rlimits.Stack,
    87  		}
    88  	})
    89  
    90  	Describe("Ping", func() {
    91  		Context("when the response is successful", func() {
    92  			BeforeEach(func() {
    93  				server.AppendHandlers(
    94  					ghttp.CombineHandlers(
    95  						ghttp.VerifyRequest("GET", "/ping"),
    96  						ghttp.RespondWith(200, "{}"),
    97  					),
    98  				)
    99  			})
   100  
   101  			It("should ping the server", func() {
   102  				err := connection.Ping()
   103  				Expect(err).ToNot(HaveOccurred())
   104  			})
   105  		})
   106  
   107  		Context("when the request fails", func() {
   108  			BeforeEach(func() {
   109  				server.AppendHandlers(
   110  					ghttp.CombineHandlers(
   111  						ghttp.VerifyRequest("GET", "/ping"),
   112  						ghttp.RespondWith(500, ""),
   113  					),
   114  				)
   115  			})
   116  
   117  			It("should return an error", func() {
   118  				err := connection.Ping()
   119  				Expect(err).To(HaveOccurred())
   120  			})
   121  		})
   122  
   123  		Context("when the request fails with special error code http.StatusServiceUnavailable", func() {
   124  			BeforeEach(func() {
   125  				server.AppendHandlers(
   126  					ghttp.CombineHandlers(
   127  						ghttp.VerifyRequest("GET", "/ping"),
   128  						ghttp.RespondWith(http.StatusGatewayTimeout, `{ "Type": "ServiceUnavailableError" , "Message": "Special Error Message"}`),
   129  					),
   130  				)
   131  			})
   132  
   133  			It("should return an error without the http info in the error message", func() {
   134  				err := connection.Ping()
   135  				Expect(err).To(MatchError("Special Error Message"))
   136  			})
   137  
   138  			It("should return an error of the appropriate type", func() {
   139  				err := connection.Ping()
   140  				Expect(err).To(BeAssignableToTypeOf(garden.ServiceUnavailableError{}))
   141  			})
   142  		})
   143  
   144  		Context("when the request fails with extra special error code http.StatusInternalServerError", func() {
   145  			BeforeEach(func() {
   146  				server.AppendHandlers(
   147  					ghttp.CombineHandlers(
   148  						ghttp.VerifyRequest("GET", "/ping"),
   149  						ghttp.RespondWith(http.StatusGatewayTimeout, `{ "Type": "UnrecoverableError" , "Message": "Extra Special Error Message"}`),
   150  					),
   151  				)
   152  			})
   153  
   154  			It("should return an error without the http info in the error message", func() {
   155  				err := connection.Ping()
   156  				Expect(err).To(MatchError("Extra Special Error Message"))
   157  			})
   158  
   159  			It("should return an error of the appropriate unrecoverable type", func() {
   160  				err := connection.Ping()
   161  				Expect(err).To(BeAssignableToTypeOf(garden.UnrecoverableError{}))
   162  			})
   163  		})
   164  	})
   165  
   166  	Describe("Getting capacity", func() {
   167  		Context("when the response is successful", func() {
   168  			BeforeEach(func() {
   169  				server.AppendHandlers(
   170  					ghttp.CombineHandlers(
   171  						ghttp.VerifyRequest("GET", "/capacity"),
   172  						ghttp.RespondWith(200, marshalProto(&garden.Capacity{
   173  							MemoryInBytes: 1111,
   174  							DiskInBytes:   2222,
   175  							MaxContainers: 42,
   176  						}))))
   177  			})
   178  
   179  			It("should return the server's capacity", func() {
   180  				capacity, err := connection.Capacity()
   181  				Expect(err).ToNot(HaveOccurred())
   182  
   183  				Expect(capacity.MemoryInBytes).To(BeNumerically("==", 1111))
   184  				Expect(capacity.DiskInBytes).To(BeNumerically("==", 2222))
   185  				Expect(capacity.MaxContainers).To(BeNumerically("==", 42))
   186  			})
   187  		})
   188  
   189  		Context("when the request fails", func() {
   190  			BeforeEach(func() {
   191  				server.AppendHandlers(
   192  					ghttp.CombineHandlers(
   193  						ghttp.VerifyRequest("GET", "/capacity"),
   194  						ghttp.RespondWith(500, "")))
   195  			})
   196  
   197  			It("should return an error", func() {
   198  				_, err := connection.Capacity()
   199  				Expect(err).To(HaveOccurred())
   200  			})
   201  		})
   202  	})
   203  
   204  	Describe("Creating", func() {
   205  		var spec garden.ContainerSpec
   206  
   207  		JustBeforeEach(func() {
   208  			server.AppendHandlers(
   209  				ghttp.CombineHandlers(
   210  					ghttp.VerifyRequest("POST", "/containers"),
   211  					verifyRequestBody(&spec, &garden.ContainerSpec{}),
   212  					ghttp.RespondWith(200, marshalProto(&struct{ Handle string }{"foohandle"}))))
   213  		})
   214  
   215  		Context("with an empty ContainerSpec", func() {
   216  			BeforeEach(func() {
   217  				spec = garden.ContainerSpec{}
   218  			})
   219  
   220  			It("sends the ContainerSpec over the connection as JSON", func() {
   221  				handle, err := connection.Create(spec)
   222  				Expect(err).ToNot(HaveOccurred())
   223  				Expect(handle).To(Equal("foohandle"))
   224  			})
   225  		})
   226  
   227  		Context("with a fully specified ContainerSpec", func() {
   228  			BeforeEach(func() {
   229  				spec = garden.ContainerSpec{
   230  					Handle:     "some-handle",
   231  					GraceTime:  10 * time.Second,
   232  					RootFSPath: "some-rootfs-path",
   233  					Network:    "some-network",
   234  					BindMounts: []garden.BindMount{
   235  						{
   236  							SrcPath: "/src-a",
   237  							DstPath: "/dst-a",
   238  							Mode:    garden.BindMountModeRO,
   239  							Origin:  garden.BindMountOriginHost,
   240  						},
   241  						{
   242  							SrcPath: "/src-b",
   243  							DstPath: "/dst-b",
   244  							Mode:    garden.BindMountModeRW,
   245  							Origin:  garden.BindMountOriginContainer,
   246  						},
   247  					},
   248  					Properties: map[string]string{
   249  						"foo": "bar",
   250  					},
   251  					Env: []string{"env1=env1Value1"},
   252  				}
   253  			})
   254  
   255  			It("sends the ContainerSpec over the connection as JSON", func() {
   256  				handle, err := connection.Create(spec)
   257  				Expect(err).ToNot(HaveOccurred())
   258  				Expect(handle).To(Equal("foohandle"))
   259  			})
   260  		})
   261  	})
   262  
   263  	Describe("Destroying", func() {
   264  		Context("when destroying succeeds", func() {
   265  			BeforeEach(func() {
   266  				server.AppendHandlers(
   267  					ghttp.CombineHandlers(
   268  						ghttp.VerifyRequest("DELETE", "/containers/foo"),
   269  						ghttp.RespondWith(200, "{}")))
   270  			})
   271  
   272  			It("should stop the container", func() {
   273  				err := connection.Destroy("foo")
   274  				Expect(err).ToNot(HaveOccurred())
   275  			})
   276  		})
   277  
   278  		Context("when destroying fails because the container doesn't exist", func() {
   279  			BeforeEach(func() {
   280  				server.AppendHandlers(
   281  					ghttp.CombineHandlers(
   282  						ghttp.VerifyRequest("DELETE", "/containers/foo"),
   283  						ghttp.RespondWith(404, `{ "Type": "ContainerNotFoundError", "Handle" : "some handle"}`)))
   284  			})
   285  
   286  			It("return an appropriate error with the message", func() {
   287  				err := connection.Destroy("foo")
   288  				Expect(err).To(MatchError(garden.ContainerNotFoundError{Handle: "some handle"}))
   289  			})
   290  		})
   291  	})
   292  
   293  	Describe("Stopping", func() {
   294  		BeforeEach(func() {
   295  			server.AppendHandlers(
   296  				ghttp.CombineHandlers(
   297  					ghttp.VerifyRequest("PUT", "/containers/foo/stop"),
   298  					verifyRequestBody(map[string]interface{}{
   299  						"kill": true,
   300  					}, make(map[string]interface{})),
   301  					ghttp.RespondWith(200, "{}")))
   302  		})
   303  
   304  		It("should stop the container", func() {
   305  			err := connection.Stop("foo", true)
   306  			Expect(err).ToNot(HaveOccurred())
   307  		})
   308  	})
   309  
   310  	Describe("fetching limit info", func() {
   311  		Describe("getting memory limits", func() {
   312  			BeforeEach(func() {
   313  				server.AppendHandlers(
   314  					ghttp.CombineHandlers(
   315  						ghttp.VerifyRequest("GET", "/containers/foo/limits/memory"),
   316  						ghttp.RespondWith(200, marshalProto(&garden.MemoryLimits{
   317  							LimitInBytes: 40,
   318  						}, &garden.MemoryLimits{})),
   319  					),
   320  				)
   321  			})
   322  
   323  			It("gets the memory limit", func() {
   324  				currentLimits, err := connection.CurrentMemoryLimits("foo")
   325  				Expect(err).ToNot(HaveOccurred())
   326  				Expect(currentLimits.LimitInBytes).To(BeNumerically("==", 40))
   327  			})
   328  		})
   329  
   330  		Describe("getting cpu limits", func() {
   331  			BeforeEach(func() {
   332  				server.AppendHandlers(
   333  					ghttp.CombineHandlers(
   334  						ghttp.VerifyRequest("GET", "/containers/foo/limits/cpu"),
   335  						ghttp.RespondWith(200, marshalProto(&garden.CPULimits{
   336  							LimitInShares: 40,
   337  						})),
   338  					),
   339  				)
   340  			})
   341  
   342  			It("gets the cpu limit", func() {
   343  				limits, err := connection.CurrentCPULimits("foo")
   344  				Expect(err).ToNot(HaveOccurred())
   345  
   346  				Expect(limits.LimitInShares).To(BeNumerically("==", 40))
   347  			})
   348  		})
   349  
   350  		Describe("getting bandwidth limits", func() {
   351  			BeforeEach(func() {
   352  				server.AppendHandlers(
   353  					ghttp.CombineHandlers(
   354  						ghttp.VerifyRequest("GET", "/containers/foo/limits/bandwidth"),
   355  						ghttp.RespondWith(200, marshalProto(&garden.BandwidthLimits{
   356  							RateInBytesPerSecond:      1,
   357  							BurstRateInBytesPerSecond: 2,
   358  						})),
   359  					),
   360  				)
   361  			})
   362  
   363  			It("gets the bandwidth limit", func() {
   364  				limits, err := connection.CurrentBandwidthLimits("foo")
   365  				Expect(err).ToNot(HaveOccurred())
   366  
   367  				Expect(limits.RateInBytesPerSecond).To(BeNumerically("==", 1))
   368  				Expect(limits.BurstRateInBytesPerSecond).To(BeNumerically("==", 2))
   369  			})
   370  		})
   371  	})
   372  
   373  	Describe("NetIn", func() {
   374  		BeforeEach(func() {
   375  			server.AppendHandlers(
   376  				ghttp.CombineHandlers(
   377  					ghttp.VerifyRequest("POST", "/containers/foo-handle/net/in"),
   378  					verifyRequestBody(map[string]interface{}{
   379  						"handle":         "foo-handle",
   380  						"host_port":      float64(8080),
   381  						"container_port": float64(8081),
   382  					}, make(map[string]interface{})),
   383  					ghttp.RespondWith(200, marshalProto(map[string]interface{}{
   384  						"host_port":      1234,
   385  						"container_port": 1235,
   386  					}))))
   387  		})
   388  
   389  		It("should return the allocated ports", func() {
   390  			hostPort, containerPort, err := connection.NetIn("foo-handle", 8080, 8081)
   391  			Expect(err).ToNot(HaveOccurred())
   392  			Expect(hostPort).To(Equal(uint32(1234)))
   393  			Expect(containerPort).To(Equal(uint32(1235)))
   394  		})
   395  	})
   396  
   397  	Describe("NetOut", func() {
   398  		var (
   399  			rule   garden.NetOutRule
   400  			handle string
   401  		)
   402  
   403  		BeforeEach(func() {
   404  			handle = "foo-handle"
   405  		})
   406  
   407  		JustBeforeEach(func() {
   408  			server.AppendHandlers(
   409  				ghttp.CombineHandlers(
   410  					ghttp.VerifyRequest("POST", fmt.Sprintf("/containers/%s/net/out", handle)),
   411  					verifyRequestBody(&rule, &garden.NetOutRule{}),
   412  					ghttp.RespondWith(200, "{}")))
   413  		})
   414  
   415  		Context("when a NetOutRule is passed", func() {
   416  			BeforeEach(func() {
   417  				rule = garden.NetOutRule{
   418  					Protocol: garden.ProtocolICMP,
   419  					Networks: []garden.IPRange{garden.IPRangeFromIP(net.ParseIP("1.2.3.4"))},
   420  					Ports:    []garden.PortRange{garden.PortRangeFromPort(2), garden.PortRangeFromPort(4)},
   421  					ICMPs:    &garden.ICMPControl{Type: 3, Code: garden.ICMPControlCode(3)},
   422  					Log:      true,
   423  				}
   424  			})
   425  
   426  			It("should send the rule over the wire", func() {
   427  				Expect(connection.NetOut(handle, rule)).To(Succeed())
   428  			})
   429  		})
   430  	})
   431  
   432  	Describe("Listing containers", func() {
   433  		BeforeEach(func() {
   434  			server.AppendHandlers(
   435  				ghttp.CombineHandlers(
   436  					ghttp.VerifyRequest("GET", "/containers", "foo=bar"),
   437  					ghttp.RespondWith(200, marshalProto(&struct {
   438  						Handles []string `json:"handles"`
   439  					}{
   440  						[]string{"container1", "container2", "container3"},
   441  					}))))
   442  		})
   443  
   444  		It("should return the list of containers", func() {
   445  			handles, err := connection.List(map[string]string{"foo": "bar"})
   446  
   447  			Expect(err).ToNot(HaveOccurred())
   448  			Expect(handles).To(Equal([]string{"container1", "container2", "container3"}))
   449  		})
   450  	})
   451  
   452  	Describe("Getting container properties", func() {
   453  		handle := "container-handle"
   454  		var status int
   455  
   456  		BeforeEach(func() {
   457  			status = 200
   458  		})
   459  
   460  		JustBeforeEach(func() {
   461  			server.AppendHandlers(
   462  				ghttp.CombineHandlers(
   463  					ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/properties", handle)),
   464  					ghttp.RespondWith(status, "{\"foo\": \"bar\"}")))
   465  		})
   466  
   467  		It("returns the map of properties", func() {
   468  			properties, err := connection.Properties(handle)
   469  
   470  			Expect(err).ToNot(HaveOccurred())
   471  			Expect(properties).To(
   472  				Equal(garden.Properties{
   473  					"foo": "bar",
   474  				}),
   475  			)
   476  		})
   477  
   478  		Context("when getting container properties fails", func() {
   479  			BeforeEach(func() {
   480  				status = 400
   481  			})
   482  
   483  			It("returns an error", func() {
   484  				_, err := connection.Properties(handle)
   485  				Expect(err).To(HaveOccurred())
   486  			})
   487  		})
   488  	})
   489  
   490  	Describe("Get container property", func() {
   491  
   492  		handle := "container-handle"
   493  		propertyName := "property_name"
   494  		propertyValue := "property_value"
   495  		var status int
   496  
   497  		BeforeEach(func() {
   498  			status = 200
   499  		})
   500  
   501  		JustBeforeEach(func() {
   502  			server.AppendHandlers(
   503  				ghttp.CombineHandlers(
   504  					ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/properties/%s", handle, propertyName)),
   505  					ghttp.RespondWith(status, fmt.Sprintf("{\"value\": \"%s\"}", propertyValue))))
   506  		})
   507  
   508  		It("returns the property", func() {
   509  			property, err := connection.Property(handle, propertyName)
   510  
   511  			Expect(err).ToNot(HaveOccurred())
   512  			Expect(property).To(Equal(propertyValue))
   513  		})
   514  
   515  		Context("when getting container property fails", func() {
   516  			BeforeEach(func() {
   517  				status = 400
   518  			})
   519  
   520  			It("returns an error", func() {
   521  				_, err := connection.Property(handle, propertyName)
   522  				Expect(err).To(HaveOccurred())
   523  			})
   524  		})
   525  
   526  	})
   527  
   528  	Describe("Getting container metrics", func() {
   529  		handle := "container-handle"
   530  		metrics := garden.Metrics{
   531  			MemoryStat: garden.ContainerMemoryStat{
   532  				Cache:                   1,
   533  				Rss:                     2,
   534  				MappedFile:              3,
   535  				Pgpgin:                  4,
   536  				Pgpgout:                 5,
   537  				Swap:                    6,
   538  				Pgfault:                 7,
   539  				Pgmajfault:              8,
   540  				InactiveAnon:            9,
   541  				ActiveAnon:              10,
   542  				InactiveFile:            11,
   543  				ActiveFile:              12,
   544  				Unevictable:             13,
   545  				HierarchicalMemoryLimit: 14,
   546  				HierarchicalMemswLimit:  15,
   547  				TotalCache:              16,
   548  				TotalRss:                17,
   549  				TotalMappedFile:         18,
   550  				TotalPgpgin:             19,
   551  				TotalPgpgout:            20,
   552  				TotalSwap:               21,
   553  				TotalPgfault:            22,
   554  				TotalPgmajfault:         23,
   555  				TotalInactiveAnon:       24,
   556  				TotalActiveAnon:         25,
   557  				TotalInactiveFile:       26,
   558  				TotalActiveFile:         27,
   559  				TotalUnevictable:        28,
   560  				TotalUsageTowardLimit:   7, // TotalRss+(TotalCache-TotalInactiveFile)
   561  			},
   562  			CPUStat: garden.ContainerCPUStat{
   563  				Usage:  1,
   564  				User:   2,
   565  				System: 3,
   566  			},
   567  
   568  			DiskStat: garden.ContainerDiskStat{
   569  				TotalBytesUsed:      11,
   570  				TotalInodesUsed:     12,
   571  				ExclusiveBytesUsed:  13,
   572  				ExclusiveInodesUsed: 14,
   573  			},
   574  		}
   575  		var status int
   576  
   577  		BeforeEach(func() {
   578  			status = 200
   579  		})
   580  
   581  		JustBeforeEach(func() {
   582  			server.AppendHandlers(
   583  				ghttp.CombineHandlers(
   584  					ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/metrics", handle)),
   585  					ghttp.RespondWith(status, marshalProto(metrics))))
   586  		})
   587  
   588  		It("returns the MemoryStat, CPUStat and DiskStat", func() {
   589  			returnedMetrics, err := connection.Metrics(handle)
   590  
   591  			Expect(err).ToNot(HaveOccurred())
   592  			Expect(returnedMetrics).To(Equal(metrics))
   593  		})
   594  
   595  		Context("when getting container metrics fails", func() {
   596  			BeforeEach(func() {
   597  				status = 400
   598  			})
   599  
   600  			It("returns an error", func() {
   601  				_, err := connection.Metrics(handle)
   602  				Expect(err).To(HaveOccurred())
   603  			})
   604  		})
   605  	})
   606  
   607  	Describe("Setting the grace time", func() {
   608  		var (
   609  			status    int
   610  			handle    string
   611  			graceTime time.Duration
   612  		)
   613  
   614  		BeforeEach(func() {
   615  			handle = "container-handle"
   616  			graceTime = time.Second * 5
   617  			status = 200
   618  		})
   619  
   620  		JustBeforeEach(func() {
   621  			server.AppendHandlers(
   622  				ghttp.CombineHandlers(
   623  					ghttp.VerifyRequest("PUT", fmt.Sprintf("/containers/%s/grace_time", handle)),
   624  					// interface{} confusion: the JSON decoder decodes numberics to float64...
   625  					verifyRequestBody(float64(graceTime), float64(0)),
   626  					ghttp.RespondWith(status, "{}"),
   627  				),
   628  			)
   629  		})
   630  
   631  		It("send SetGraceTime request", func() {
   632  			Expect(connection.SetGraceTime(handle, graceTime)).To(Succeed())
   633  		})
   634  
   635  		Context("when setting grace time fails", func() {
   636  			BeforeEach(func() {
   637  				status = 400
   638  			})
   639  
   640  			It("returns an error", func() {
   641  				Expect(connection.SetGraceTime(handle, graceTime)).ToNot(Succeed())
   642  			})
   643  		})
   644  	})
   645  
   646  	Describe("Getting container info", func() {
   647  		var infoResponse garden.ContainerInfo
   648  
   649  		JustBeforeEach(func() {
   650  			infoResponse = garden.ContainerInfo{
   651  				State: "chilling out",
   652  				Events: []string{
   653  					"maxing",
   654  					"relaxing all cool",
   655  				},
   656  				HostIP:        "host-ip",
   657  				ContainerIP:   "container-ip",
   658  				ContainerPath: "container-path",
   659  				ProcessIDs:    []string{"process-handle-1", "process-handle-2"},
   660  				Properties: garden.Properties{
   661  					"prop-key": "prop-value",
   662  				},
   663  				MappedPorts: []garden.PortMapping{
   664  					{HostPort: 1234, ContainerPort: 5678},
   665  					{HostPort: 1235, ContainerPort: 5679},
   666  				},
   667  			}
   668  
   669  			server.AppendHandlers(
   670  				ghttp.CombineHandlers(
   671  					ghttp.VerifyRequest("GET", "/containers/some-handle/info"),
   672  					ghttp.RespondWith(200, marshalProto(infoResponse))))
   673  		})
   674  
   675  		It("should return the container's info", func() {
   676  			info, err := connection.Info("some-handle")
   677  			Expect(err).ToNot(HaveOccurred())
   678  
   679  			Expect(info).To(Equal(infoResponse))
   680  		})
   681  	})
   682  
   683  	Describe("BulkInfo", func() {
   684  
   685  		expectedBulkInfo := map[string]garden.ContainerInfoEntry{
   686  			"handle1": garden.ContainerInfoEntry{
   687  				Info: garden.ContainerInfo{
   688  					State: "container1state",
   689  				},
   690  			},
   691  			"handle2": garden.ContainerInfoEntry{
   692  				Info: garden.ContainerInfo{
   693  					State: "container2state",
   694  				},
   695  			},
   696  		}
   697  
   698  		handles := []string{"handle1", "handle2"}
   699  		queryParams := "handles=" + strings.Join(handles, "%2C")
   700  
   701  		Context("when the response is successful", func() {
   702  			JustBeforeEach(func() {
   703  				server.AppendHandlers(
   704  					ghttp.CombineHandlers(
   705  						ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams),
   706  						ghttp.RespondWith(200, marshalProto(expectedBulkInfo))))
   707  			})
   708  
   709  			It("returns info about containers", func() {
   710  				bulkInfo, err := connection.BulkInfo(handles)
   711  				Expect(err).ToNot(HaveOccurred())
   712  				Expect(bulkInfo).To(Equal(expectedBulkInfo))
   713  			})
   714  		})
   715  
   716  		Context("when the request fails", func() {
   717  			JustBeforeEach(func() {
   718  				server.AppendHandlers(
   719  					ghttp.CombineHandlers(
   720  						ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams),
   721  						ghttp.RespondWith(500, ""),
   722  					),
   723  				)
   724  			})
   725  
   726  			It("returns the error", func() {
   727  				_, err := connection.BulkInfo(handles)
   728  				Expect(err).To(HaveOccurred())
   729  			})
   730  		})
   731  
   732  		Context("when a container is in error state", func() {
   733  			It("returns the error for the container", func() {
   734  
   735  				expectedBulkInfo := map[string]garden.ContainerInfoEntry{
   736  					"error": garden.ContainerInfoEntry{
   737  						Err: &garden.Error{Err: errors.New("Oopps")},
   738  					},
   739  					"success": garden.ContainerInfoEntry{
   740  						Info: garden.ContainerInfo{
   741  							State: "container2state",
   742  						},
   743  					},
   744  				}
   745  
   746  				server.AppendHandlers(
   747  					ghttp.CombineHandlers(
   748  						ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams),
   749  						ghttp.RespondWith(200, marshalProto(expectedBulkInfo))))
   750  
   751  				bulkInfo, err := connection.BulkInfo(handles)
   752  				Expect(err).ToNot(HaveOccurred())
   753  				Expect(bulkInfo).To(Equal(expectedBulkInfo))
   754  			})
   755  		})
   756  	})
   757  
   758  	Describe("BulkMetrics", func() {
   759  
   760  		expectedBulkMetrics := map[string]garden.ContainerMetricsEntry{
   761  			"handle1": garden.ContainerMetricsEntry{
   762  				Metrics: garden.Metrics{
   763  					DiskStat: garden.ContainerDiskStat{
   764  						TotalInodesUsed:     4,
   765  						TotalBytesUsed:      3,
   766  						ExclusiveBytesUsed:  2,
   767  						ExclusiveInodesUsed: 1,
   768  					},
   769  				},
   770  			},
   771  			"handle2": garden.ContainerMetricsEntry{
   772  				Metrics: garden.Metrics{
   773  					DiskStat: garden.ContainerDiskStat{
   774  						TotalInodesUsed:     5,
   775  						TotalBytesUsed:      6,
   776  						ExclusiveBytesUsed:  7,
   777  						ExclusiveInodesUsed: 8,
   778  					},
   779  				},
   780  			},
   781  		}
   782  
   783  		handles := []string{"handle1", "handle2"}
   784  		queryParams := "handles=" + strings.Join(handles, "%2C")
   785  
   786  		Context("when the response is successful", func() {
   787  			JustBeforeEach(func() {
   788  				server.AppendHandlers(
   789  					ghttp.CombineHandlers(
   790  						ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams),
   791  						ghttp.RespondWith(200, marshalProto(expectedBulkMetrics))))
   792  			})
   793  
   794  			It("returns info about containers", func() {
   795  				bulkMetrics, err := connection.BulkMetrics(handles)
   796  				Expect(err).ToNot(HaveOccurred())
   797  				Expect(bulkMetrics).To(Equal(expectedBulkMetrics))
   798  			})
   799  		})
   800  
   801  		Context("when the request fails", func() {
   802  			JustBeforeEach(func() {
   803  				server.AppendHandlers(
   804  					ghttp.CombineHandlers(
   805  						ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams),
   806  						ghttp.RespondWith(500, ""),
   807  					),
   808  				)
   809  			})
   810  
   811  			It("returns the error", func() {
   812  				_, err := connection.BulkMetrics(handles)
   813  				Expect(err).To(HaveOccurred())
   814  			})
   815  		})
   816  
   817  		Context("when a container has an error", func() {
   818  			It("returns the error for the container", func() {
   819  
   820  				errorBulkMetrics := map[string]garden.ContainerMetricsEntry{
   821  					"error": garden.ContainerMetricsEntry{
   822  						Err: &garden.Error{Err: errors.New("Oh noes!")},
   823  					},
   824  					"success": garden.ContainerMetricsEntry{
   825  						Metrics: garden.Metrics{
   826  							DiskStat: garden.ContainerDiskStat{
   827  								TotalInodesUsed: 1,
   828  							},
   829  						},
   830  					},
   831  				}
   832  
   833  				server.AppendHandlers(
   834  					ghttp.CombineHandlers(
   835  						ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams),
   836  						ghttp.RespondWith(200, marshalProto(errorBulkMetrics))))
   837  
   838  				bulkMetrics, err := connection.BulkMetrics(handles)
   839  				Expect(err).ToNot(HaveOccurred())
   840  				Expect(bulkMetrics).To(Equal(errorBulkMetrics))
   841  			})
   842  		})
   843  	})
   844  
   845  	Describe("Streaming in", func() {
   846  		Context("when streaming in succeeds", func() {
   847  			BeforeEach(func() {
   848  				server.AppendHandlers(
   849  					ghttp.CombineHandlers(
   850  						ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=alice&destination=%2Fbar"),
   851  						func(w http.ResponseWriter, r *http.Request) {
   852  							body, err := ioutil.ReadAll(r.Body)
   853  							Expect(err).ToNot(HaveOccurred())
   854  
   855  							Expect(string(body)).To(Equal("chunk-1chunk-2"))
   856  						},
   857  					),
   858  				)
   859  			})
   860  
   861  			It("tells garden.to stream, and then streams the content as a series of chunks", func() {
   862  				buffer := bytes.NewBufferString("chunk-1chunk-2")
   863  
   864  				err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "alice", Path: "/bar", TarStream: buffer})
   865  				Expect(err).ToNot(HaveOccurred())
   866  
   867  				Expect(server.ReceivedRequests()).To(HaveLen(1))
   868  			})
   869  		})
   870  
   871  		Context("when streaming in returns an error response", func() {
   872  			BeforeEach(func() {
   873  				server.AppendHandlers(
   874  					ghttp.CombineHandlers(
   875  						ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=bob&destination=%2Fbar"),
   876  						ghttp.RespondWith(http.StatusInternalServerError, "no."),
   877  					),
   878  				)
   879  			})
   880  
   881  			It("returns an error on close", func() {
   882  				buffer := bytes.NewBufferString("chunk-1chunk-2")
   883  				err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "bob", Path: "/bar", TarStream: buffer})
   884  				Expect(err).To(HaveOccurred())
   885  
   886  				Expect(server.ReceivedRequests()).To(HaveLen(1))
   887  			})
   888  		})
   889  
   890  		Context("when streaming in fails hard", func() {
   891  			BeforeEach(func() {
   892  				server.AppendHandlers(
   893  					ghttp.CombineHandlers(
   894  						ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=bob&destination=%2Fbar"),
   895  						ghttp.RespondWith(http.StatusInternalServerError, "no."),
   896  						func(w http.ResponseWriter, r *http.Request) {
   897  							server.CloseClientConnections()
   898  						},
   899  					),
   900  				)
   901  			})
   902  
   903  			It("returns an error on close", func() {
   904  				buffer := bytes.NewBufferString("chunk-1chunk-2")
   905  
   906  				err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "bob", Path: "/bar", TarStream: buffer})
   907  				Expect(err).To(HaveOccurred())
   908  
   909  				Expect(server.ReceivedRequests()).To(HaveLen(1))
   910  			})
   911  		})
   912  	})
   913  
   914  	Describe("Streaming Out", func() {
   915  		Context("when streaming succeeds", func() {
   916  			BeforeEach(func() {
   917  				server.AppendHandlers(
   918  					ghttp.CombineHandlers(
   919  						ghttp.VerifyRequest("GET", "/containers/foo-handle/files", "user=frank&source=%2Fbar"),
   920  						ghttp.RespondWith(200, "hello-world!"),
   921  					),
   922  				)
   923  			})
   924  
   925  			It("asks garden.for the given file, then reads its content", func() {
   926  				reader, err := connection.StreamOut("foo-handle", garden.StreamOutSpec{User: "frank", Path: "/bar"})
   927  				Expect(err).ToNot(HaveOccurred())
   928  
   929  				readBytes, err := ioutil.ReadAll(reader)
   930  				Expect(err).ToNot(HaveOccurred())
   931  				Expect(readBytes).To(Equal([]byte("hello-world!")))
   932  
   933  				reader.Close()
   934  			})
   935  		})
   936  
   937  		Context("when streaming fails", func() {
   938  			BeforeEach(func() {
   939  				server.AppendHandlers(
   940  					ghttp.CombineHandlers(
   941  						ghttp.VerifyRequest("GET", "/containers/foo-handle/files", "user=deandra&source=%2Fbar"),
   942  						func(w http.ResponseWriter, r *http.Request) {
   943  							w.Header().Set("Content-Length", "500")
   944  						},
   945  					),
   946  				)
   947  			})
   948  
   949  			It("asks garden.for the given file, then reads its content", func() {
   950  				reader, err := connection.StreamOut("foo-handle", garden.StreamOutSpec{User: "deandra", Path: "/bar"})
   951  				Expect(err).ToNot(HaveOccurred())
   952  
   953  				_, err = ioutil.ReadAll(reader)
   954  				reader.Close()
   955  				Expect(err).To(HaveOccurred())
   956  			})
   957  		})
   958  	})
   959  
   960  	Describe("Running", func() {
   961  		var (
   962  			spec         garden.ProcessSpec
   963  			stdInContent chan string
   964  		)
   965  
   966  		Context("when streaming succeeds to completion", func() {
   967  			BeforeEach(func() {
   968  				spec = garden.ProcessSpec{
   969  					Path:   "lol",
   970  					Args:   []string{"arg1", "arg2"},
   971  					Dir:    "/some/dir",
   972  					User:   "root",
   973  					Limits: resourceLimits,
   974  				}
   975  				stdInContent = make(chan string)
   976  
   977  				server.AppendHandlers(
   978  					ghttp.CombineHandlers(
   979  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
   980  						ghttp.VerifyJSONRepresenting(spec),
   981  						func(w http.ResponseWriter, r *http.Request) {
   982  							w.WriteHeader(http.StatusOK)
   983  
   984  							conn, br, err := w.(http.Hijacker).Hijack()
   985  							Expect(err).ToNot(HaveOccurred())
   986  
   987  							defer conn.Close()
   988  
   989  							decoder := json.NewDecoder(br)
   990  
   991  							transport.WriteMessage(conn, map[string]interface{}{
   992  								"process_id": "process-handle",
   993  								"stream_id":  "123",
   994  							})
   995  
   996  							var payload map[string]interface{}
   997  							err = decoder.Decode(&payload)
   998  							Expect(err).ToNot(HaveOccurred())
   999  
  1000  							Expect(payload).To(Equal(map[string]interface{}{
  1001  								"process_id": "process-handle",
  1002  								"source":     float64(transport.Stdin),
  1003  								"data":       "stdin data",
  1004  							}))
  1005  							stdInContent <- payload["data"].(string)
  1006  
  1007  							transport.WriteMessage(conn, map[string]interface{}{
  1008  								"process_id":  "process-handle",
  1009  								"exit_status": 3,
  1010  							})
  1011  						},
  1012  					),
  1013  					stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1014  						conn.Write([]byte("stdout data"))
  1015  						conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-stdInContent)))
  1016  					}),
  1017  					stderrStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1018  						conn.Write([]byte("stderr data"))
  1019  					}),
  1020  				)
  1021  			})
  1022  
  1023  			It("streams the data, closes the destinations, and notifies of exit", func() {
  1024  				stdout := gbytes.NewBuffer()
  1025  				stderr := gbytes.NewBuffer()
  1026  
  1027  				process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{
  1028  					Stdin:  bytes.NewBufferString("stdin data"),
  1029  					Stdout: stdout,
  1030  					Stderr: stderr,
  1031  				})
  1032  
  1033  				Expect(err).ToNot(HaveOccurred())
  1034  				Expect(process.ID()).To(Equal("process-handle"))
  1035  
  1036  				Eventually(stdout).Should(gbytes.Say("stdout data"))
  1037  				Eventually(stdout).Should(gbytes.Say("roundtripped stdin data"))
  1038  				Eventually(stderr).Should(gbytes.Say("stderr data"))
  1039  
  1040  				status, err := process.Wait()
  1041  				Expect(err).ToNot(HaveOccurred())
  1042  				Expect(status).To(Equal(3))
  1043  			})
  1044  
  1045  			It("finishes streaming stdout and stderr before returning from .Wait", func() {
  1046  				stdout := gbytes.NewBuffer()
  1047  				stderr := gbytes.NewBuffer()
  1048  
  1049  				process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{
  1050  					Stdin:  bytes.NewBufferString("stdin data"),
  1051  					Stdout: stdout,
  1052  					Stderr: stderr,
  1053  				})
  1054  				Expect(err).ToNot(HaveOccurred())
  1055  
  1056  				process.Wait()
  1057  				Expect(stdout).To(gbytes.Say("roundtripped stdin data"))
  1058  				Expect(stderr).To(gbytes.Say("stderr data"))
  1059  			})
  1060  
  1061  			Describe("connection leak avoidance", func() {
  1062  				var fakeHijacker *connectionfakes.FakeHijackStreamer
  1063  				var wrappedConnections []*wrappedConnection
  1064  
  1065  				BeforeEach(func() {
  1066  					wrappedConnections = []*wrappedConnection{}
  1067  					netHijacker := hijacker
  1068  					fakeHijacker = new(connectionfakes.FakeHijackStreamer)
  1069  					fakeHijacker.HijackStub = func(ctx context.Context, handler string, body io.Reader, params rata.Params, query url.Values, contentType string) (net.Conn, *bufio.Reader, error) {
  1070  						conn, resp, err := netHijacker.Hijack(ctx, handler, body, params, query, contentType)
  1071  						wc := &wrappedConnection{Conn: conn}
  1072  						wrappedConnections = append(wrappedConnections, wc)
  1073  						return wc, resp, err
  1074  					}
  1075  
  1076  					hijacker = fakeHijacker
  1077  				})
  1078  
  1079  				It("should not leak net.Conn from Run", func() {
  1080  					stdout := gbytes.NewBuffer()
  1081  					stderr := gbytes.NewBuffer()
  1082  
  1083  					process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{
  1084  						Stdin:  bytes.NewBufferString("stdin data"),
  1085  						Stdout: stdout,
  1086  						Stderr: stderr,
  1087  					})
  1088  					Expect(err).ToNot(HaveOccurred())
  1089  
  1090  					process.Wait()
  1091  					Expect(stdout).To(gbytes.Say("roundtripped stdin data"))
  1092  					Expect(stderr).To(gbytes.Say("stderr data"))
  1093  
  1094  					for _, wc := range wrappedConnections {
  1095  						Eventually(wc.isClosed).Should(BeTrue())
  1096  					}
  1097  				})
  1098  			})
  1099  		})
  1100  
  1101  		Context("when the process is terminated", func() {
  1102  			BeforeEach(func() {
  1103  				server.AppendHandlers(
  1104  					ghttp.CombineHandlers(
  1105  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1106  						func(w http.ResponseWriter, r *http.Request) {
  1107  							w.WriteHeader(http.StatusOK)
  1108  
  1109  							conn, br, err := w.(http.Hijacker).Hijack()
  1110  							Expect(err).ToNot(HaveOccurred())
  1111  
  1112  							defer conn.Close()
  1113  
  1114  							decoder := json.NewDecoder(br)
  1115  
  1116  							transport.WriteMessage(conn, map[string]interface{}{
  1117  								"process_id": "process-handle",
  1118  								"stream_id":  "123",
  1119  							})
  1120  
  1121  							var payload map[string]interface{}
  1122  							err = decoder.Decode(&payload)
  1123  							Expect(err).ToNot(HaveOccurred())
  1124  
  1125  							Expect(payload).To(Equal(map[string]interface{}{
  1126  								"process_id": "process-handle",
  1127  								"signal":     float64(garden.SignalTerminate),
  1128  							}))
  1129  
  1130  							transport.WriteMessage(conn, map[string]interface{}{
  1131  								"process_id":  "process-handle",
  1132  								"exit_status": 3,
  1133  							})
  1134  						},
  1135  					),
  1136  					stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1137  						conn.Write([]byte("stdout data"))
  1138  						conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-stdInContent)))
  1139  					}),
  1140  					emptyStderrStream("foo-handle", "process-handle", 123),
  1141  				)
  1142  			})
  1143  
  1144  			It("sends the appropriate protocol message", func() {
  1145  				process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{}, garden.ProcessIO{})
  1146  
  1147  				Expect(err).ToNot(HaveOccurred())
  1148  				Expect(process.ID()).To(Equal("process-handle"))
  1149  
  1150  				err = process.Signal(garden.SignalTerminate)
  1151  				Expect(err).ToNot(HaveOccurred())
  1152  
  1153  				status, err := process.Wait()
  1154  				Expect(err).ToNot(HaveOccurred())
  1155  				Expect(status).To(Equal(3))
  1156  			})
  1157  		})
  1158  
  1159  		Context("when the process is killed", func() {
  1160  			BeforeEach(func() {
  1161  				server.AppendHandlers(
  1162  					ghttp.CombineHandlers(
  1163  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1164  						func(w http.ResponseWriter, r *http.Request) {
  1165  							w.WriteHeader(http.StatusOK)
  1166  
  1167  							conn, br, err := w.(http.Hijacker).Hijack()
  1168  							Expect(err).ToNot(HaveOccurred())
  1169  
  1170  							defer conn.Close()
  1171  
  1172  							decoder := json.NewDecoder(br)
  1173  
  1174  							transport.WriteMessage(conn, map[string]interface{}{
  1175  								"process_id": "process-handle",
  1176  								"stream_id":  "123",
  1177  							})
  1178  
  1179  							var payload map[string]interface{}
  1180  							err = decoder.Decode(&payload)
  1181  							Expect(err).ToNot(HaveOccurred())
  1182  
  1183  							Expect(payload).To(Equal(map[string]interface{}{
  1184  								"process_id": "process-handle",
  1185  								"signal":     float64(garden.SignalKill),
  1186  							}))
  1187  
  1188  							transport.WriteMessage(conn, map[string]interface{}{
  1189  								"process_id":  "process-handle",
  1190  								"exit_status": 3,
  1191  							})
  1192  						},
  1193  					),
  1194  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1195  					emptyStderrStream("foo-handle", "process-handle", 123),
  1196  				)
  1197  			})
  1198  
  1199  			It("sends the appropriate protocol message", func() {
  1200  				process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{}, garden.ProcessIO{})
  1201  
  1202  				Expect(err).ToNot(HaveOccurred())
  1203  				Expect(process.ID()).To(Equal("process-handle"))
  1204  
  1205  				err = process.Signal(garden.SignalKill)
  1206  				Expect(err).ToNot(HaveOccurred())
  1207  
  1208  				status, err := process.Wait()
  1209  				Expect(err).ToNot(HaveOccurred())
  1210  				Expect(status).To(Equal(3))
  1211  			})
  1212  		})
  1213  
  1214  		Context("when the process's window is resized", func() {
  1215  			var spec garden.ProcessSpec
  1216  			BeforeEach(func() {
  1217  				spec = garden.ProcessSpec{
  1218  					Path: "lol",
  1219  					Args: []string{"arg1", "arg2"},
  1220  					TTY: &garden.TTYSpec{
  1221  						WindowSize: &garden.WindowSize{
  1222  							Columns: 100,
  1223  							Rows:    200,
  1224  						},
  1225  					},
  1226  				}
  1227  
  1228  				server.AppendHandlers(
  1229  					ghttp.CombineHandlers(
  1230  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1231  						ghttp.VerifyJSONRepresenting(spec),
  1232  						func(w http.ResponseWriter, r *http.Request) {
  1233  							w.WriteHeader(http.StatusOK)
  1234  
  1235  							conn, br, err := w.(http.Hijacker).Hijack()
  1236  							Expect(err).ToNot(HaveOccurred())
  1237  
  1238  							defer conn.Close()
  1239  
  1240  							decoder := json.NewDecoder(br)
  1241  
  1242  							transport.WriteMessage(conn, map[string]interface{}{
  1243  								"process_id": "process-handle",
  1244  								"stream_id":  "123",
  1245  							})
  1246  
  1247  							// the stdin data may come in before or after the tty message
  1248  							Eventually(func() interface{} {
  1249  								var payload map[string]interface{}
  1250  								err = decoder.Decode(&payload)
  1251  								Expect(err).ToNot(HaveOccurred())
  1252  
  1253  								return payload
  1254  							}).Should(Equal(map[string]interface{}{
  1255  								"process_id": "process-handle",
  1256  								"tty": map[string]interface{}{
  1257  									"window_size": map[string]interface{}{
  1258  										"columns": float64(80),
  1259  										"rows":    float64(24),
  1260  									},
  1261  								},
  1262  							}))
  1263  
  1264  							transport.WriteMessage(conn, map[string]interface{}{
  1265  								"process_id":  "process-handle",
  1266  								"exit_status": 3,
  1267  							})
  1268  						},
  1269  					),
  1270  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1271  					emptyStderrStream("foo-handle", "process-handle", 123),
  1272  				)
  1273  			})
  1274  
  1275  			It("sends the appropriate protocol message", func() {
  1276  				process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{
  1277  					Stdin:  bytes.NewBufferString("stdin data"),
  1278  					Stdout: gbytes.NewBuffer(),
  1279  					Stderr: gbytes.NewBuffer(),
  1280  				})
  1281  
  1282  				Expect(err).ToNot(HaveOccurred())
  1283  				Expect(process.ID()).To(Equal("process-handle"))
  1284  
  1285  				err = process.SetTTY(garden.TTYSpec{
  1286  					WindowSize: &garden.WindowSize{
  1287  						Columns: 80,
  1288  						Rows:    24,
  1289  					},
  1290  				})
  1291  				Expect(err).ToNot(HaveOccurred())
  1292  
  1293  				status, err := process.Wait()
  1294  				Expect(err).ToNot(HaveOccurred())
  1295  				Expect(status).To(Equal(3))
  1296  			})
  1297  		})
  1298  
  1299  		Context("when the connection breaks while attaching to the streams", func() {
  1300  			BeforeEach(func() {
  1301  				server.AppendHandlers(
  1302  					ghttp.CombineHandlers(
  1303  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1304  						func(w http.ResponseWriter, r *http.Request) {
  1305  							w.WriteHeader(http.StatusOK)
  1306  
  1307  							conn, _, err := w.(http.Hijacker).Hijack()
  1308  							Expect(err).ToNot(HaveOccurred())
  1309  
  1310  							defer conn.Close()
  1311  
  1312  							transport.WriteMessage(conn, map[string]interface{}{
  1313  								"process_id": "process-handle",
  1314  								"stream_id":  "123",
  1315  							})
  1316  						},
  1317  					),
  1318  					ghttp.CombineHandlers(
  1319  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle/attaches/123/stdout"),
  1320  
  1321  						func(w http.ResponseWriter, r *http.Request) {
  1322  							w.WriteHeader(http.StatusInternalServerError)
  1323  
  1324  							conn, _, err := w.(http.Hijacker).Hijack()
  1325  							Expect(err).ToNot(HaveOccurred())
  1326  							defer conn.Close()
  1327  						},
  1328  					),
  1329  				)
  1330  			})
  1331  
  1332  			Describe("waiting on the process", func() {
  1333  				It("returns an error", func(done Done) {
  1334  					process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{
  1335  						Path: "lol",
  1336  						Args: []string{"arg1", "arg2"},
  1337  						Dir:  "/some/dir",
  1338  					}, garden.ProcessIO{Stdout: GinkgoWriter})
  1339  
  1340  					Expect(err).ToNot(HaveOccurred())
  1341  
  1342  					_, err = process.Wait()
  1343  					Expect(err).To(MatchError(ContainSubstring("connection: failed to hijack stream ")))
  1344  
  1345  					close(done)
  1346  				})
  1347  			})
  1348  		})
  1349  
  1350  		Context("when the connection breaks before an exit status is received", func() {
  1351  			BeforeEach(func() {
  1352  				server.AppendHandlers(
  1353  					ghttp.CombineHandlers(
  1354  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1355  						func(w http.ResponseWriter, r *http.Request) {
  1356  							w.WriteHeader(http.StatusOK)
  1357  
  1358  							conn, _, err := w.(http.Hijacker).Hijack()
  1359  							Expect(err).ToNot(HaveOccurred())
  1360  
  1361  							defer conn.Close()
  1362  
  1363  							transport.WriteMessage(conn, map[string]interface{}{
  1364  								"process_id": "process-handle",
  1365  								"stream_id":  "123",
  1366  							})
  1367  						},
  1368  					),
  1369  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1370  					emptyStderrStream("foo-handle", "process-handle", 123),
  1371  				)
  1372  			})
  1373  
  1374  			Describe("waiting on the process", func() {
  1375  				It("returns an error", func() {
  1376  					process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{
  1377  						Path: "lol",
  1378  						Args: []string{"arg1", "arg2"},
  1379  						Dir:  "/some/dir",
  1380  					}, garden.ProcessIO{})
  1381  
  1382  					Expect(err).ToNot(HaveOccurred())
  1383  
  1384  					_, err = process.Wait()
  1385  					Expect(err).To(HaveOccurred())
  1386  				})
  1387  			})
  1388  		})
  1389  
  1390  		Context("when the connection returns an error payload", func() {
  1391  			BeforeEach(func() {
  1392  				server.AppendHandlers(
  1393  					ghttp.CombineHandlers(
  1394  						ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1395  						ghttp.RespondWith(200, marshalProto(map[string]interface{}{
  1396  							"process_id": "process-handle",
  1397  							"stream_id":  "123",
  1398  						},
  1399  							map[string]interface{}{
  1400  								"process_id": "process-handle",
  1401  								"source":     transport.Stderr,
  1402  								"data":       "stderr data",
  1403  							},
  1404  							map[string]interface{}{
  1405  								"process_id": "process-handle",
  1406  								"error":      "oh no!",
  1407  							},
  1408  						)),
  1409  					),
  1410  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1411  					emptyStderrStream("foo-handle", "process-handle", 123),
  1412  				)
  1413  			})
  1414  
  1415  			Describe("waiting on the process", func() {
  1416  				It("returns an error", func() {
  1417  					process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{
  1418  						Path: "lol",
  1419  						Args: []string{"arg1", "arg2"},
  1420  						Dir:  "/some/dir",
  1421  					}, garden.ProcessIO{})
  1422  
  1423  					Expect(err).ToNot(HaveOccurred())
  1424  
  1425  					_, err = process.Wait()
  1426  					Expect(err).To(HaveOccurred())
  1427  					Expect(err.Error()).To(ContainSubstring("oh no!"))
  1428  				})
  1429  			})
  1430  		})
  1431  
  1432  		Context("when the connection returns an error status", func() {
  1433  			BeforeEach(func() {
  1434  				server.AppendHandlers(ghttp.CombineHandlers(
  1435  					ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"),
  1436  					ghttp.RespondWith(500, "an error occurred!"),
  1437  				))
  1438  			})
  1439  
  1440  			It("returns an error", func() {
  1441  				_, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{
  1442  					Path: "lol",
  1443  					Args: []string{"arg1", "arg2"},
  1444  					Dir:  "/some/dir",
  1445  				}, garden.ProcessIO{})
  1446  
  1447  				Expect(err).To(MatchError(ContainSubstring("an error occurred!")))
  1448  			})
  1449  		})
  1450  	})
  1451  
  1452  	Describe("Attaching", func() {
  1453  		Context("when streaming succeeds to completion", func() {
  1454  			BeforeEach(func() {
  1455  				expectedRoundtrip := make(chan string)
  1456  				server.AppendHandlers(
  1457  					ghttp.CombineHandlers(
  1458  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"),
  1459  						func(w http.ResponseWriter, r *http.Request) {
  1460  							w.WriteHeader(http.StatusOK)
  1461  
  1462  							conn, br, err := w.(http.Hijacker).Hijack()
  1463  							Expect(err).ToNot(HaveOccurred())
  1464  
  1465  							defer conn.Close()
  1466  
  1467  							transport.WriteMessage(conn, map[string]interface{}{
  1468  								"process_id": "process-handle",
  1469  								"stream_id":  "123",
  1470  							})
  1471  
  1472  							var payload map[string]interface{}
  1473  							err = json.NewDecoder(br).Decode(&payload)
  1474  							Expect(err).ToNot(HaveOccurred())
  1475  
  1476  							Expect(payload).To(Equal(map[string]interface{}{
  1477  								"process_id": "process-handle",
  1478  								"source":     float64(transport.Stdin),
  1479  								"data":       "stdin data",
  1480  							}))
  1481  							expectedRoundtrip <- payload["data"].(string)
  1482  
  1483  							transport.WriteMessage(conn, map[string]interface{}{
  1484  								"process_id":  "process-handle",
  1485  								"exit_status": 3,
  1486  							})
  1487  						},
  1488  					),
  1489  					stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1490  						conn.Write([]byte("stdout data"))
  1491  						conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-expectedRoundtrip)))
  1492  					}),
  1493  					stderrStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1494  						conn.Write([]byte("stderr data"))
  1495  					}),
  1496  				)
  1497  			})
  1498  
  1499  			It("should stream", func() {
  1500  				stdout := gbytes.NewBuffer()
  1501  				stderr := gbytes.NewBuffer()
  1502  
  1503  				process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{
  1504  					Stdin:  bytes.NewBufferString("stdin data"),
  1505  					Stdout: stdout,
  1506  					Stderr: stderr,
  1507  				})
  1508  
  1509  				Expect(err).ToNot(HaveOccurred())
  1510  				Expect(process.ID()).To(Equal("process-handle"))
  1511  
  1512  				Eventually(stdout).Should(gbytes.Say("stdout data"))
  1513  				Eventually(stderr).Should(gbytes.Say("stderr data"))
  1514  				Eventually(stdout).Should(gbytes.Say("roundtripped stdin data"))
  1515  
  1516  				status, err := process.Wait()
  1517  				Expect(err).ToNot(HaveOccurred())
  1518  				Expect(status).To(Equal(3))
  1519  			})
  1520  
  1521  			It("finishes streaming stdout and stderr before returning from .Wait", func() {
  1522  				stdout := gbytes.NewBuffer()
  1523  				stderr := gbytes.NewBuffer()
  1524  
  1525  				process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{
  1526  					Stdin:  bytes.NewBufferString("stdin data"),
  1527  					Stdout: stdout,
  1528  					Stderr: stderr,
  1529  				})
  1530  
  1531  				Expect(err).ToNot(HaveOccurred())
  1532  
  1533  				process.Wait()
  1534  				Expect(stdout).To(gbytes.Say("roundtripped stdin data"))
  1535  				Expect(stderr).To(gbytes.Say("stderr data"))
  1536  			})
  1537  
  1538  		})
  1539  
  1540  		Context("when ctx is done", func() {
  1541  
  1542  			var fakeHijacker *connectionfakes.FakeHijackStreamer
  1543  			var wrappedConnections []*wrappedConnection
  1544  			var streamCancelFunc context.CancelFunc
  1545  			var streamContext context.Context
  1546  
  1547  			BeforeEach(func() {
  1548  				streamContext, streamCancelFunc = context.WithCancel(context.Background())
  1549  				server.AppendHandlers(
  1550  					ghttp.CombineHandlers(
  1551  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"),
  1552  						func(w http.ResponseWriter, r *http.Request) {
  1553  							w.WriteHeader(http.StatusOK)
  1554  
  1555  							conn, _, err := w.(http.Hijacker).Hijack()
  1556  							Expect(err).ToNot(HaveOccurred())
  1557  
  1558  							defer conn.Close()
  1559  
  1560  							transport.WriteMessage(conn, map[string]interface{}{
  1561  								"process_id": "process-handle",
  1562  								"stream_id":  "123",
  1563  							})
  1564  						},
  1565  					),
  1566  					stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) {
  1567  						<-streamContext.Done()
  1568  						conn.Write([]byte("stdout data"))
  1569  					}),
  1570  					emptyStderrStream("foo-handle", "process-handle", 123),
  1571  				)
  1572  				wrappedConnections = []*wrappedConnection{}
  1573  				netHijacker := hijacker
  1574  				fakeHijacker = new(connectionfakes.FakeHijackStreamer)
  1575  				fakeHijacker.HijackStub = func(ctx context.Context, handler string, body io.Reader, params rata.Params, query url.Values, contentType string) (net.Conn, *bufio.Reader, error) {
  1576  					conn, resp, err := netHijacker.Hijack(ctx, handler, body, params, query, contentType)
  1577  					wc := &wrappedConnection{Conn: conn}
  1578  					wrappedConnections = append(wrappedConnections, wc)
  1579  					return wc, resp, err
  1580  				}
  1581  
  1582  				hijacker = fakeHijacker
  1583  			})
  1584  			AfterEach(func() {
  1585  				streamCancelFunc()
  1586  			})
  1587  
  1588  			It("should close all net.Conn from Attach and return from .Wait", func() {
  1589  				ctx, cancelFunc := context.WithCancel(context.Background())
  1590  				process, err := connection.Attach(ctx, "foo-handle", "process-handle", garden.ProcessIO{
  1591  					Stdin:  bytes.NewBufferString("stdin data"),
  1592  					Stdout: gbytes.NewBuffer(),
  1593  					Stderr: gbytes.NewBuffer(),
  1594  				})
  1595  				Expect(err).ToNot(HaveOccurred())
  1596  				go func() {
  1597  					cancelFunc()
  1598  				}()
  1599  				process.Wait()
  1600  
  1601  				for _, wc := range wrappedConnections {
  1602  					Eventually(wc.isClosed).Should(BeTrue())
  1603  				}
  1604  			})
  1605  		})
  1606  
  1607  		Context("when an error occurs while reading the given stdin stream", func() {
  1608  			It("does not send an EOF to close the process's stdin", func() {
  1609  				finishedReq := make(chan struct{})
  1610  
  1611  				server.AppendHandlers(
  1612  					ghttp.CombineHandlers(
  1613  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"),
  1614  						func(w http.ResponseWriter, r *http.Request) {
  1615  							w.WriteHeader(http.StatusOK)
  1616  
  1617  							conn, br, err := w.(http.Hijacker).Hijack()
  1618  							Expect(err).ToNot(HaveOccurred())
  1619  							defer conn.Close()
  1620  
  1621  							transport.WriteMessage(conn, map[string]interface{}{
  1622  								"process_id": "process-handle",
  1623  								"stream_id":  "123",
  1624  							})
  1625  
  1626  							decoder := json.NewDecoder(br)
  1627  
  1628  							var payload map[string]interface{}
  1629  							err = decoder.Decode(&payload)
  1630  							Expect(err).ToNot(HaveOccurred())
  1631  
  1632  							Expect(payload).To(Equal(map[string]interface{}{
  1633  								"process_id": "process-handle",
  1634  								"source":     float64(transport.Stdin),
  1635  								"data":       "stdin data",
  1636  							}))
  1637  
  1638  							close(finishedReq)
  1639  						},
  1640  					),
  1641  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1642  					emptyStderrStream("foo-handle", "process-handle", 123),
  1643  				)
  1644  
  1645  				stdinR, stdinW := io.Pipe()
  1646  
  1647  				_, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{
  1648  					Stdin: stdinR,
  1649  				})
  1650  				Expect(err).ToNot(HaveOccurred())
  1651  
  1652  				stdinW.Write([]byte("stdin data"))
  1653  				stdinW.CloseWithError(errors.New("connection broke"))
  1654  
  1655  				Eventually(finishedReq).Should(BeClosed())
  1656  			})
  1657  		})
  1658  
  1659  		Context("when the connection returns an error payload", func() {
  1660  			BeforeEach(func() {
  1661  				server.AppendHandlers(
  1662  					ghttp.CombineHandlers(
  1663  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"),
  1664  						ghttp.RespondWith(200, marshalProto(map[string]interface{}{
  1665  							"process_id": "process-handle",
  1666  							"stream_id":  "123",
  1667  						},
  1668  							map[string]interface{}{
  1669  								"process_id": "process-handle",
  1670  							},
  1671  							map[string]interface{}{
  1672  								"process_id": "process-handle",
  1673  								"source":     transport.Stdout,
  1674  								"data":       "stdout data",
  1675  							},
  1676  							map[string]interface{}{
  1677  								"process_id": "process-handle",
  1678  								"source":     transport.Stderr,
  1679  								"data":       "stderr data",
  1680  							},
  1681  							map[string]interface{}{
  1682  								"process_id": "process-handle",
  1683  								"error":      "oh no!",
  1684  							},
  1685  						)),
  1686  					),
  1687  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1688  					emptyStderrStream("foo-handle", "process-handle", 123),
  1689  				)
  1690  			})
  1691  
  1692  			Describe("waiting on the process", func() {
  1693  				It("returns an error", func() {
  1694  					process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{})
  1695  
  1696  					Expect(err).ToNot(HaveOccurred())
  1697  					Expect(process.ID()).To(Equal("process-handle"))
  1698  
  1699  					_, err = process.Wait()
  1700  					Expect(err).To(HaveOccurred())
  1701  					Expect(err.Error()).To(ContainSubstring("oh no!"))
  1702  				})
  1703  			})
  1704  		})
  1705  
  1706  		Context("when the connection breaks before an exit status is received", func() {
  1707  			BeforeEach(func() {
  1708  				server.AppendHandlers(
  1709  					ghttp.CombineHandlers(
  1710  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"),
  1711  						func(w http.ResponseWriter, r *http.Request) {
  1712  							w.WriteHeader(http.StatusOK)
  1713  
  1714  							conn, _, err := w.(http.Hijacker).Hijack()
  1715  							Expect(err).ToNot(HaveOccurred())
  1716  
  1717  							defer conn.Close()
  1718  
  1719  							transport.WriteMessage(conn, map[string]interface{}{
  1720  								"process_id": "process-handle",
  1721  								"stream_id":  "123",
  1722  							})
  1723  
  1724  							transport.WriteMessage(conn, map[string]interface{}{
  1725  								"process_id": "process-handle",
  1726  								"source":     transport.Stdout,
  1727  								"data":       "stdout data",
  1728  							})
  1729  
  1730  							transport.WriteMessage(conn, map[string]interface{}{
  1731  								"process_id": "process-handle",
  1732  								"source":     transport.Stderr,
  1733  								"data":       "stderr data",
  1734  							})
  1735  						},
  1736  					),
  1737  					emptyStdoutStream("foo-handle", "process-handle", 123),
  1738  					emptyStderrStream("foo-handle", "process-handle", 123),
  1739  				)
  1740  			})
  1741  
  1742  			Describe("waiting on the process", func() {
  1743  				It("returns an error", func() {
  1744  					process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{})
  1745  
  1746  					Expect(err).ToNot(HaveOccurred())
  1747  					Expect(process.ID()).To(Equal("process-handle"))
  1748  
  1749  					_, err = process.Wait()
  1750  					Expect(err).To(HaveOccurred())
  1751  				})
  1752  			})
  1753  		})
  1754  
  1755  		Context("when the server returns HTTP 404", func() {
  1756  			BeforeEach(func() {
  1757  				gardenErr := garden.Error{Err: garden.ProcessNotFoundError{ProcessID: "idontexist"}}
  1758  				respBody, err := gardenErr.MarshalJSON()
  1759  				Expect(err).NotTo(HaveOccurred())
  1760  				server.AppendHandlers(
  1761  					ghttp.CombineHandlers(
  1762  						ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/idontexist"),
  1763  						ghttp.RespondWith(http.StatusNotFound, respBody),
  1764  					),
  1765  				)
  1766  			})
  1767  
  1768  			It("returns a ProcessNotFoundError", func() {
  1769  				_, err := connection.Attach(context.TODO(), "foo-handle", "idontexist", garden.ProcessIO{})
  1770  				Expect(err).To(MatchError(garden.ProcessNotFoundError{
  1771  					ProcessID: "idontexist",
  1772  				}))
  1773  			})
  1774  		})
  1775  	})
  1776  })
  1777  
  1778  func verifyRequestBody(expectedMessage interface{}, emptyType interface{}) http.HandlerFunc {
  1779  	return func(resp http.ResponseWriter, req *http.Request) {
  1780  		defer GinkgoRecover()
  1781  
  1782  		decoder := json.NewDecoder(req.Body)
  1783  
  1784  		received := emptyType
  1785  		err := decoder.Decode(&received)
  1786  		Expect(err).ToNot(HaveOccurred())
  1787  
  1788  		Expect(received).To(Equal(expectedMessage))
  1789  	}
  1790  }
  1791  
  1792  func marshalProto(messages ...interface{}) string {
  1793  	result := new(bytes.Buffer)
  1794  	for _, msg := range messages {
  1795  		err := transport.WriteMessage(result, msg)
  1796  		Expect(err).ToNot(HaveOccurred())
  1797  	}
  1798  
  1799  	return result.String()
  1800  }
  1801  
  1802  func emptyStdoutStream(handle, processid string, attachid int) http.HandlerFunc {
  1803  	return stdoutStream(handle, processid, attachid, func(net.Conn) {})
  1804  }
  1805  
  1806  func emptyStderrStream(handle, processid string, attachid int) http.HandlerFunc {
  1807  	return stderrStream(handle, processid, attachid, func(net.Conn) {})
  1808  }
  1809  
  1810  func stderrStream(handle, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc {
  1811  	return stream(handle, "stderr", processid, attachid, fn)
  1812  }
  1813  
  1814  func stdoutStream(handle, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc {
  1815  	return stream(handle, "stdout", processid, attachid, fn)
  1816  }
  1817  
  1818  func stream(handle, route, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc {
  1819  	return ghttp.CombineHandlers(
  1820  		ghttp.VerifyRequest("GET",
  1821  			fmt.Sprintf("/containers/%s/processes/%s/attaches/%d/%s",
  1822  				handle,
  1823  				processid,
  1824  				attachid,
  1825  				route,
  1826  			)),
  1827  
  1828  		func(w http.ResponseWriter, r *http.Request) {
  1829  			w.WriteHeader(http.StatusOK)
  1830  
  1831  			conn, _, err := w.(http.Hijacker).Hijack()
  1832  			Expect(err).ToNot(HaveOccurred())
  1833  			defer conn.Close()
  1834  
  1835  			fn(conn)
  1836  		},
  1837  	)
  1838  }