[ARVADOS] updated: 98feead9acd503a81c69b06bf07d6c1bfd3dd458

git at public.curoverse.com git at public.curoverse.com
Mon Aug 10 20:46:40 EDT 2015


Summary of changes:
 .../install-crunch-dispatch.html.textile.liquid    |  36 ++---
 doc/install/install-keepproxy.html.textile.liquid  |  11 +-
 .../running-external-program.html.textile.liquid   |  10 ++
 sdk/go/arvadosclient/arvadosclient.go              | 155 ++++++++-------------
 sdk/go/arvadosclient/arvadosclient_test.go         |  30 ++++
 sdk/go/arvadosclient/pool.go                       |  13 ++
 sdk/go/httpserver/log.go                           |  11 +-
 sdk/go/keepclient/hashcheck.go                     |  24 ++--
 sdk/go/keepclient/support.go                       |   1 -
 services/arv-git-httpd/auth_handler.go             |  10 +-
 services/arv-git-httpd/server_test.go              |  16 ++-
 11 files changed, 176 insertions(+), 141 deletions(-)

  discards  be4e5f225cf1615504ee2c1feb0f200d6904716c (commit)
  discards  12c47d0c0dc38a9e1d1e5a0e953a226a1a0557c6 (commit)
  discards  847293a0e90fed989b9dea9a99f00126415530b3 (commit)
  discards  ad05e4948fab822910fbf57f60b739f100a5cdb1 (commit)
  discards  d9434ed5ae6f129227a74cc85dc15fa6bdf199ac (commit)
  discards  8b8eb200a3e8d52f1fb98142771412447bb2911e (commit)
  discards  99146504cd618769de59176ee7458c8973877242 (commit)
  discards  ca8e6a099f43d0b227fd1983dabbdaa6cbfb7246 (commit)
  discards  e024bcf310e61819a75e1ef3e45cf99b6457cfb0 (commit)
       via  98feead9acd503a81c69b06bf07d6c1bfd3dd458 (commit)
       via  8e671a545fd8abbf74afa109c0150c1d5772a207 (commit)
       via  bd583d21bb62894a5960b10bf81b375fe6336267 (commit)
       via  1e5f8ef2a8c594eb0de874bef72d6d1485725b5a (commit)
       via  e736def390001e01ff6887acbb3c6f08366f6a91 (commit)
       via  6423d36f00308b5c313715d8ddcc160052010a1c (commit)
       via  fbe23d045022aac8ff3ae691052af25968680944 (commit)
       via  fe4f75a44988826afa194c68b455c685d3b3fa8c (commit)
       via  c2f718800dbb03d336c0370631e9ea81dbb3997f (commit)
       via  9f45d7dd8adfac9a2f690de6a0831498cff5512f (commit)
       via  e14e011f667d314e557c580de69a271534b6149f (commit)
       via  2dfb886e960cf918e54b5f03477f464afb322a9b (commit)
       via  173ebc1102e6a5a5c3a26c1bb231a4b035713369 (commit)
       via  e889ec14bbd18cf82acfabc681d0db967772692d (commit)
       via  d2e546749afaa1ff8fff8cb920b9a54d58154b76 (commit)
       via  522404b2066c8b635a240f79dd4bf652479afadb (commit)
       via  862843135810f7ba4bc6c2138f2ba1bb8840e432 (commit)
       via  f618b6517fff37194a1a4183a607658f46952732 (commit)
       via  2d9cae58ae387d29d161ea96b6cd704d0764b1d8 (commit)
       via  7c89127a0214c77578c09bac9ac24ce9e1c5e104 (commit)
       via  70ddebda3dbe90c8a347c9077397106c6fb949c0 (commit)
       via  a6edcb025f0b659e464fd3e98e59e966ced88afd (commit)
       via  ae2478d6d09d2ab2eac1acceab8342f4f900ec8b (commit)
       via  2f719adbd0eb4ce07fc2c7012bc1ce8df02e790b (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (be4e5f225cf1615504ee2c1feb0f200d6904716c)
            \
             N -- N -- N (98feead9acd503a81c69b06bf07d6c1bfd3dd458)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 98feead9acd503a81c69b06bf07d6c1bfd3dd458
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 10 20:45:38 2015 -0400

    5824: Add test for nonexistent resource type.

diff --git a/sdk/go/arvadosclient/arvadosclient_test.go b/sdk/go/arvadosclient/arvadosclient_test.go
index 2783b9d..249505e 100644
--- a/sdk/go/arvadosclient/arvadosclient_test.go
+++ b/sdk/go/arvadosclient/arvadosclient_test.go
@@ -56,6 +56,26 @@ func (s *ServerRequiredSuite) TestGetEmptyUUID(c *C) {
 	c.Assert(len(getback), Equals, 0)
 }
 
+func (s *ServerRequiredSuite) TestInvalidResourceType(c *C) {
+	arv, err := MakeArvadosClient()
+
+	getback := make(Dict)
+	err = arv.Get("unicorns", "zzzzz-zebra-unicorn7unicorn", nil, &getback)
+	c.Assert(err, FitsTypeOf, APIServerError{})
+	c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
+	c.Assert(len(getback), Equals, 0)
+
+	err = arv.Update("unicorns", "zzzzz-zebra-unicorn7unicorn", nil, &getback)
+	c.Assert(err, FitsTypeOf, APIServerError{})
+	c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
+	c.Assert(len(getback), Equals, 0)
+
+	err = arv.List("unicorns", nil, &getback)
+	c.Assert(err, FitsTypeOf, APIServerError{})
+	c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
+	c.Assert(len(getback), Equals, 0)
+}
+
 func (s *ServerRequiredSuite) TestCreatePipelineTemplate(c *C) {
 	arv, err := MakeArvadosClient()
 

commit 8e671a545fd8abbf74afa109c0150c1d5772a207
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 10 20:39:39 2015 -0400

    5824: Document ClientPool.

diff --git a/sdk/go/arvadosclient/pool.go b/sdk/go/arvadosclient/pool.go
index 1c5893a..87b67c3 100644
--- a/sdk/go/arvadosclient/pool.go
+++ b/sdk/go/arvadosclient/pool.go
@@ -4,11 +4,17 @@ import (
 	"sync"
 )
 
+// A ClientPool is a pool of ArvadosClients. This is useful for
+// applications that make API calls using a dynamic set of tokens,
+// like web services that pass through their own clients'
+// credentials. See arvados-git-httpd for an example, and sync.Pool
+// for more information about garbage collection.
 type ClientPool struct {
 	sync.Pool
 	lastErr error
 }
 
+// MakeClientPool returns a new empty ClientPool.
 func MakeClientPool() *ClientPool {
 	p := &ClientPool{}
 	p.Pool = sync.Pool{New: func() interface{} {
@@ -22,10 +28,16 @@ func MakeClientPool() *ClientPool {
 	return p
 }
 
+// Err returns the error that was encountered last time Get returned
+// nil.
 func (p *ClientPool) Err() error {
 	return p.lastErr
 }
 
+// Get returns an ArvadosClient taken from the pool, or a new one if
+// the pool is empty. If an existing client is returned, its state
+// (including its ApiToken) will be just as it was when it was Put
+// back in the pool.
 func (p *ClientPool) Get() *ArvadosClient {
 	c, ok := p.Pool.Get().(*ArvadosClient)
 	if !ok {
@@ -34,6 +46,7 @@ func (p *ClientPool) Get() *ArvadosClient {
 	return c
 }
 
+// Put puts an ArvadosClient back in the pool.
 func (p *ClientPool) Put(c *ArvadosClient) {
 	p.Pool.Put(c)
 }

commit bd583d21bb62894a5960b10bf81b375fe6336267
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 10 20:39:25 2015 -0400

    5824: Doc/comment fixes, add test for "Get empty UUID"

diff --git a/sdk/go/arvadosclient/arvadosclient.go b/sdk/go/arvadosclient/arvadosclient.go
index 9bdf54c..e43162f 100644
--- a/sdk/go/arvadosclient/arvadosclient.go
+++ b/sdk/go/arvadosclient/arvadosclient.go
@@ -103,30 +103,21 @@ func MakeArvadosClient() (ac ArvadosClient, err error) {
 	return ac, err
 }
 
-// Low-level access to a resource.
-//
-//   method - HTTP method, one of GET, HEAD, PUT, POST or DELETE
-//   resource - the arvados resource to act on
-//   uuid - the uuid of the specific item to access (may be empty)
-//   action - sub-action to take on the resource or uuid (may be empty)
-//   parameters - method parameters
-//
-// return
-//   reader - the body reader, or nil if there was an error
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) CallRaw(method string, resource string, uuid string, action string, parameters Dict) (reader io.ReadCloser, err error) {
+// CallRaw is the same as Call() but returns a Reader that reads the
+// response body, instead of taking an output object.
+func (c ArvadosClient) CallRaw(method string, resourceType string, uuid string, action string, parameters Dict) (reader io.ReadCloser, err error) {
 	var req *http.Request
 
 	u := url.URL{
 		Scheme: "https",
-		Host:   this.ApiServer}
+		Host:   c.ApiServer}
 
-	if resource != API_DISCOVERY_RESOURCE {
+	if resourceType != API_DISCOVERY_RESOURCE {
 		u.Path = "/arvados/v1"
 	}
 
-	if resource != "" {
-		u.Path = u.Path + "/" + resource
+	if resourceType != "" {
+		u.Path = u.Path + "/" + resourceType
 	}
 	if uuid != "" {
 		u.Path = u.Path + "/" + uuid
@@ -161,14 +152,14 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
 	}
 
 	// Add api token header
-	req.Header.Add("Authorization", fmt.Sprintf("OAuth2 %s", this.ApiToken))
-	if this.External {
+	req.Header.Add("Authorization", fmt.Sprintf("OAuth2 %s", c.ApiToken))
+	if c.External {
 		req.Header.Add("X-External-Client", "1")
 	}
 
 	// Make the request
 	var resp *http.Response
-	if resp, err = this.Client.Do(req); err != nil {
+	if resp, err = c.Client.Do(req); err != nil {
 		return nil, err
 	}
 
@@ -177,7 +168,7 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
 	}
 
 	defer resp.Body.Close()
-	return nil, newAPIServerError(this.ApiServer, resp)
+	return nil, newAPIServerError(c.ApiServer, resp)
 }
 
 func newAPIServerError(ServerAddress string, resp *http.Response) APIServerError {
@@ -209,19 +200,20 @@ func newAPIServerError(ServerAddress string, resp *http.Response) APIServerError
 	return ase
 }
 
-// Access to a resource.
+// Call an API endpoint and parse the JSON response into an object.
 //
-//   method - HTTP method, one of GET, HEAD, PUT, POST or DELETE
-//   resource - the arvados resource to act on
-//   uuid - the uuid of the specific item to access (may be empty)
-//   action - sub-action to take on the resource or uuid (may be empty)
-//   parameters - method parameters
-//   output - a map or annotated struct which is a legal target for encoding/json/Decoder
-// return
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) Call(method string, resource string, uuid string, action string, parameters Dict, output interface{}) (err error) {
-	var reader io.ReadCloser
-	reader, err = this.CallRaw(method, resource, uuid, action, parameters)
+//   method - HTTP method: GET, HEAD, PUT, POST, PATCH or DELETE.
+//   resourceType - the type of arvados resource to act on (e.g., "collections", "pipeline_instances").
+//   uuid - the uuid of the specific item to access. May be empty.
+//   action - API method name (e.g., "lock"). This is often empty if implied by method and uuid.
+//   parameters - method parameters.
+//   output - a map or annotated struct which is a legal target for encoding/json/Decoder.
+//
+// Returns a non-nil error if an error occurs making the API call, the
+// API responds with a non-successful HTTP status, or an error occurs
+// parsing the response body.
+func (c ArvadosClient) Call(method string, resourceType string, uuid string, action string, parameters Dict, output interface{}) error {
+	reader, err := c.CallRaw(method, resourceType, uuid, action, parameters)
 	if reader != nil {
 		defer reader.Close()
 	}
@@ -238,81 +230,56 @@ func (this ArvadosClient) Call(method string, resource string, uuid string, acti
 	return nil
 }
 
-// Create a new instance of a resource.
-//
-//   resource - the arvados resource on which to create an item
-//   parameters - method parameters
-//   output - a map or annotated struct which is a legal target for encoding/json/Decoder
-// return
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) Create(resource string, parameters Dict, output interface{}) (err error) {
-	return this.Call("POST", resource, "", "", parameters, output)
+// Create a new resource. See Call for argument descriptions.
+func (c ArvadosClient) Create(resourceType string, parameters Dict, output interface{}) error {
+	return c.Call("POST", resourceType, "", "", parameters, output)
 }
 
-// Delete an instance of a resource.
-//
-//   resource - the arvados resource on which to delete an item
-//   uuid - the item to delete
-//   parameters - method parameters
-//   output - a map or annotated struct which is a legal target for encoding/json/Decoder
-// return
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) Delete(resource string, uuid string, parameters Dict, output interface{}) (err error) {
-	return this.Call("DELETE", resource, uuid, "", parameters, output)
+// Delete a resource. See Call for argument descriptions.
+func (c ArvadosClient) Delete(resource string, uuid string, parameters Dict, output interface{}) (err error) {
+	return c.Call("DELETE", resource, uuid, "", parameters, output)
 }
 
-// Update fields of an instance of a resource.
-//
-//   resource - the arvados resource on which to update the item
-//   uuid - the item to update
-//   parameters - method parameters
-//   output - a map or annotated struct which is a legal target for encoding/json/Decoder
-// return
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) Update(resource string, uuid string, parameters Dict, output interface{}) (err error) {
-	return this.Call("PUT", resource, uuid, "", parameters, output)
+// Modify attributes of a resource. See Call for argument descriptions.
+func (c ArvadosClient) Update(resourceType string, uuid string, parameters Dict, output interface{}) (err error) {
+	return c.Call("PUT", resourceType, uuid, "", parameters, output)
 }
 
-// Get a resource.
-func (c ArvadosClient) Get(resource string, uuid string, parameters Dict, output interface{}) (err error) {
+// Get a resource. See Call for argument descriptions.
+func (c ArvadosClient) Get(resourceType string, uuid string, parameters Dict, output interface{}) (err error) {
 	if uuid == "" {
-		// There's no endpoint for that because GET /type/ is
-		// the List API. If there were an endpoint, the
-		// response would be 404: no object has uuid == "".
+		// No object has uuid == "": there is no need to make
+		// an API call. Furthermore, the HTTP request for such
+		// an API call would be "GET /arvados/v1/type/", which
+		// is liable to be misinterpreted as the List API.
 		return APIServerError{
 			ServerAddress:     c.ApiServer,
-			HttpStatusCode:    404,
+			HttpStatusCode:    http.StatusNotFound,
 			HttpStatusMessage: "Not Found",
 		}
 	}
-	return c.Call("GET", resource, uuid, "", parameters, output)
+	return c.Call("GET", resourceType, uuid, "", parameters, output)
 }
 
-// List the instances of a resource
-//
-//   resource - the arvados resource on which to list
-//   parameters - method parameters
-//   output - a map or annotated struct which is a legal target for encoding/json/Decoder
-// return
-//   err - error accessing the resource, or nil if no error
-func (this ArvadosClient) List(resource string, parameters Dict, output interface{}) (err error) {
-	return this.Call("GET", resource, "", "", parameters, output)
+// List resources of a given type. See Call for argument descriptions.
+func (c ArvadosClient) List(resource string, parameters Dict, output interface{}) (err error) {
+	return c.Call("GET", resource, "", "", parameters, output)
 }
 
 const API_DISCOVERY_RESOURCE = "discovery/v1/apis/arvados/v1/rest"
 
 // Discovery returns the value of the given parameter in the discovery document.
-func (this *ArvadosClient) Discovery(parameter string) (value interface{}, err error) {
-	if len(this.DiscoveryDoc) == 0 {
-		this.DiscoveryDoc = make(Dict)
-		err = this.Call("GET", API_DISCOVERY_RESOURCE, "", "", nil, &this.DiscoveryDoc)
+func (c *ArvadosClient) Discovery(parameter string) (value interface{}, err error) {
+	if len(c.DiscoveryDoc) == 0 {
+		c.DiscoveryDoc = make(Dict)
+		err = c.Call("GET", API_DISCOVERY_RESOURCE, "", "", nil, &c.DiscoveryDoc)
 		if err != nil {
 			return nil, err
 		}
 	}
 
 	var found bool
-	value, found = this.DiscoveryDoc[parameter]
+	value, found = c.DiscoveryDoc[parameter]
 	if found {
 		return value, nil
 	} else {
diff --git a/sdk/go/arvadosclient/arvadosclient_test.go b/sdk/go/arvadosclient/arvadosclient_test.go
index c2cf83e..2783b9d 100644
--- a/sdk/go/arvadosclient/arvadosclient_test.go
+++ b/sdk/go/arvadosclient/arvadosclient_test.go
@@ -46,6 +46,16 @@ func (s *ServerRequiredSuite) TestMakeArvadosClientInsecure(c *C) {
 	c.Check(kc.Client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, Equals, true)
 }
 
+func (s *ServerRequiredSuite) TestGetEmptyUUID(c *C) {
+	arv, err := MakeArvadosClient()
+
+	getback := make(Dict)
+	err = arv.Get("collections", "", nil, &getback)
+	c.Assert(err, FitsTypeOf, APIServerError{})
+	c.Assert(err.(APIServerError).HttpStatusCode, Equals, http.StatusNotFound)
+	c.Assert(len(getback), Equals, 0)
+}
+
 func (s *ServerRequiredSuite) TestCreatePipelineTemplate(c *C) {
 	arv, err := MakeArvadosClient()
 

commit 1e5f8ef2a8c594eb0de874bef72d6d1485725b5a
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 10 19:51:27 2015 -0400

    5824: Fix up comments for godoc.

diff --git a/sdk/go/keepclient/hashcheck.go b/sdk/go/keepclient/hashcheck.go
index 1f696d9..1706134 100644
--- a/sdk/go/keepclient/hashcheck.go
+++ b/sdk/go/keepclient/hashcheck.go
@@ -1,8 +1,3 @@
-// Lightweight implementation of io.ReadCloser that checks the contents read
-// from the underlying io.Reader a against checksum hash.  To avoid reading the
-// entire contents into a buffer up front, the hash is updated with each read,
-// and the actual checksum is not checked until the underlying reader returns
-// EOF.
 package keepclient
 
 import (
@@ -14,20 +9,22 @@ import (
 
 var BadChecksum = errors.New("Reader failed checksum")
 
+// HashCheckingReader is an io.ReadCloser that checks the contents
+// read from the underlying io.Reader against the provided hash.
 type HashCheckingReader struct {
 	// The underlying data source
 	io.Reader
 
-	// The hashing function to use
+	// The hash function to use
 	hash.Hash
 
 	// The hash value to check against.  Must be a hex-encoded lowercase string.
 	Check string
 }
 
-// Read from the underlying reader, update the hashing function, and pass the
-// results through.  Will return BadChecksum on the last read instead of EOF if
-// the checksum doesn't match.
+// Reads from the underlying reader, update the hashing function, and
+// pass the results through. Returns BadChecksum (instead of EOF) on
+// the last read if the checksum doesn't match.
 func (this HashCheckingReader) Read(p []byte) (n int, err error) {
 	n, err = this.Reader.Read(p)
 	if n > 0 {
@@ -42,8 +39,8 @@ func (this HashCheckingReader) Read(p []byte) (n int, err error) {
 	return n, err
 }
 
-// Write entire contents of this.Reader to 'dest'.  Returns BadChecksum if the
-// data written to 'dest' doesn't match the hash code of this.Check.
+// WriteTo writes the entire contents of this.Reader to dest.  Returns
+// BadChecksum if the checksum doesn't match.
 func (this HashCheckingReader) WriteTo(dest io.Writer) (written int64, err error) {
 	if writeto, ok := this.Reader.(io.WriterTo); ok {
 		written, err = writeto.WriteTo(io.MultiWriter(dest, this.Hash))
@@ -60,8 +57,9 @@ func (this HashCheckingReader) WriteTo(dest io.Writer) (written int64, err error
 	return written, err
 }
 
-// Close() the underlying Reader if it is castable to io.ReadCloser.  This will
-// drain the underlying reader of any remaining data and check the checksum.
+// Close reads all remaining data from the underlying Reader and
+// returns BadChecksum if the checksum doesn't match. It also closes
+// the underlying Reader if it implements io.ReadCloser.
 func (this HashCheckingReader) Close() (err error) {
 	_, err = io.Copy(this.Hash, this.Reader)
 
diff --git a/sdk/go/keepclient/support.go b/sdk/go/keepclient/support.go
index 7b96341..b467d06 100644
--- a/sdk/go/keepclient/support.go
+++ b/sdk/go/keepclient/support.go
@@ -1,4 +1,3 @@
-/* Internal methods to support keepclient.go */
 package keepclient
 
 import (

commit e736def390001e01ff6887acbb3c6f08366f6a91
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 3 00:39:28 2015 -0400

    5824: Add UUID and PDH matchers.

diff --git a/sdk/go/arvadosclient/arvadosclient.go b/sdk/go/arvadosclient/arvadosclient.go
index 2c2cc84..9bdf54c 100644
--- a/sdk/go/arvadosclient/arvadosclient.go
+++ b/sdk/go/arvadosclient/arvadosclient.go
@@ -16,7 +16,11 @@ import (
 	"strings"
 )
 
-// Errors
+type StringMatcher func(string) bool
+
+var UUIDMatch StringMatcher = regexp.MustCompile(`^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}$`).MatchString
+var PDHMatch StringMatcher = regexp.MustCompile(`^[0-9a-f]{32}\+\d+$`).MatchString
+
 var MissingArvadosApiHost = errors.New("Missing required environment variable ARVADOS_API_HOST")
 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
 

commit 6423d36f00308b5c313715d8ddcc160052010a1c
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 3 00:38:36 2015 -0400

    5824: Do not JSON-encode string params. Fixup comments for godoc.

diff --git a/sdk/go/arvadosclient/arvadosclient.go b/sdk/go/arvadosclient/arvadosclient.go
index c262be1..2c2cc84 100644
--- a/sdk/go/arvadosclient/arvadosclient.go
+++ b/sdk/go/arvadosclient/arvadosclient.go
@@ -135,12 +135,11 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
 		parameters = make(Dict)
 	}
 
-	parameters["format"] = "json"
-
 	vals := make(url.Values)
 	for k, v := range parameters {
-		m, err := json.Marshal(v)
-		if err == nil {
+		if s, ok := v.(string); ok {
+			vals.Set(k, s)
+		} else if m, err := json.Marshal(v); err == nil {
 			vals.Set(k, string(m))
 		}
 	}
@@ -296,14 +295,9 @@ func (this ArvadosClient) List(resource string, parameters Dict, output interfac
 	return this.Call("GET", resource, "", "", parameters, output)
 }
 
-// API Discovery
-//
-//   parameter - name of parameter to be discovered
-// return
-//   value - value of the discovered parameter
-//   err - error accessing the resource, or nil if no error
-var API_DISCOVERY_RESOURCE string = "discovery/v1/apis/arvados/v1/rest"
+const API_DISCOVERY_RESOURCE = "discovery/v1/apis/arvados/v1/rest"
 
+// Discovery returns the value of the given parameter in the discovery document.
 func (this *ArvadosClient) Discovery(parameter string) (value interface{}, err error) {
 	if len(this.DiscoveryDoc) == 0 {
 		this.DiscoveryDoc = make(Dict)

commit fbe23d045022aac8ff3ae691052af25968680944
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Aug 3 00:36:46 2015 -0400

    5824: Use fmt "%+q" instead of custom escaping.

diff --git a/sdk/go/httpserver/log.go b/sdk/go/httpserver/log.go
index 7bee887..cdfc595 100644
--- a/sdk/go/httpserver/log.go
+++ b/sdk/go/httpserver/log.go
@@ -1,20 +1,21 @@
 package httpserver
 
 import (
+	"fmt"
 	"log"
-	"strings"
 )
 
-var escaper = strings.NewReplacer("\"", "\\\"", "\\", "\\\\", "\n", "\\n")
-
 // Log calls log.Println but first transforms strings so they are
 // safer to write in logs (e.g., 'foo"bar' becomes
-// '"foo\"bar"'). Non-string args are left alone.
+// '"foo\"bar"'). Arguments that aren't strings and don't have a
+// (String() string) method are left alone.
 func Log(args ...interface{}) {
 	newargs := make([]interface{}, len(args))
 	for i, arg := range args {
 		if s, ok := arg.(string); ok {
-			newargs[i] = "\"" + escaper.Replace(s) + "\""
+			newargs[i] = fmt.Sprintf("%+q", s)
+		} else if s, ok := arg.(fmt.Stringer); ok {
+			newargs[i] = fmt.Sprintf("%+q", s.String())
 		} else {
 			newargs[i] = arg
 		}

commit fe4f75a44988826afa194c68b455c685d3b3fa8c
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Jul 23 00:01:52 2015 -0400

    5824: Add Get() method to arvadosclient.

diff --git a/sdk/go/arvadosclient/arvadosclient.go b/sdk/go/arvadosclient/arvadosclient.go
index 99b0818..c262be1 100644
--- a/sdk/go/arvadosclient/arvadosclient.go
+++ b/sdk/go/arvadosclient/arvadosclient.go
@@ -270,6 +270,21 @@ func (this ArvadosClient) Update(resource string, uuid string, parameters Dict,
 	return this.Call("PUT", resource, uuid, "", parameters, output)
 }
 
+// Get a resource.
+func (c ArvadosClient) Get(resource string, uuid string, parameters Dict, output interface{}) (err error) {
+	if uuid == "" {
+		// There's no endpoint for that because GET /type/ is
+		// the List API. If there were an endpoint, the
+		// response would be 404: no object has uuid == "".
+		return APIServerError{
+			ServerAddress:     c.ApiServer,
+			HttpStatusCode:    404,
+			HttpStatusMessage: "Not Found",
+		}
+	}
+	return c.Call("GET", resource, uuid, "", parameters, output)
+}
+
 // List the instances of a resource
 //
 //   resource - the arvados resource on which to list
diff --git a/sdk/go/arvadosclient/arvadosclient_test.go b/sdk/go/arvadosclient/arvadosclient_test.go
index 6a9e13b..c2cf83e 100644
--- a/sdk/go/arvadosclient/arvadosclient_test.go
+++ b/sdk/go/arvadosclient/arvadosclient_test.go
@@ -62,6 +62,13 @@ func (s *ServerRequiredSuite) TestCreatePipelineTemplate(c *C) {
 	c.Assert(getback["components"].(map[string]interface{})["c2"].(map[string]interface{})["script"], Equals, "script2")
 
 	uuid := getback["uuid"].(string)
+
+	getback = make(Dict)
+	err = arv.Get("pipeline_templates", uuid, nil, &getback)
+	c.Assert(err, Equals, nil)
+	c.Assert(getback["name"], Equals, "tmp")
+	c.Assert(getback["components"].(map[string]interface{})["c1"].(map[string]interface{})["script"], Equals, "script1")
+
 	getback = make(Dict)
 	err = arv.Update("pipeline_templates", uuid,
 		Dict{

commit c2f718800dbb03d336c0370631e9ea81dbb3997f
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Jun 17 00:47:12 2015 -0400

    Shut down API server after suite (noticed during 5824, otherwise no issue #)

diff --git a/services/arv-git-httpd/server_test.go b/services/arv-git-httpd/server_test.go
index beabedd..743bb1c 100644
--- a/services/arv-git-httpd/server_test.go
+++ b/services/arv-git-httpd/server_test.go
@@ -103,6 +103,10 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 	arvadostest.StartAPI()
 }
 
+func (s *IntegrationSuite) TearDownSuite(c *check.C) {
+	arvadostest.StopAPI()
+}
+
 func (s *IntegrationSuite) SetUpTest(c *check.C) {
 	arvadostest.ResetEnv()
 	s.testServer = &server{}

commit 9f45d7dd8adfac9a2f690de6a0831498cff5512f
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Jun 12 02:43:19 2015 -0400

    5824: Move client pool to SDK.

diff --git a/sdk/go/arvadosclient/pool.go b/sdk/go/arvadosclient/pool.go
new file mode 100644
index 0000000..1c5893a
--- /dev/null
+++ b/sdk/go/arvadosclient/pool.go
@@ -0,0 +1,39 @@
+package arvadosclient
+
+import (
+	"sync"
+)
+
+type ClientPool struct {
+	sync.Pool
+	lastErr error
+}
+
+func MakeClientPool() *ClientPool {
+	p := &ClientPool{}
+	p.Pool = sync.Pool{New: func() interface{} {
+		arv, err := MakeArvadosClient()
+		if err != nil {
+			p.lastErr = err
+			return nil
+		}
+		return &arv
+	}}
+	return p
+}
+
+func (p *ClientPool) Err() error {
+	return p.lastErr
+}
+
+func (p *ClientPool) Get() *ArvadosClient {
+	c, ok := p.Pool.Get().(*ArvadosClient)
+	if !ok {
+		return nil
+	}
+	return c
+}
+
+func (p *ClientPool) Put(c *ArvadosClient) {
+	p.Pool.Put(c)
+}
diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index 741e543..39a9098 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -6,7 +6,6 @@ import (
 	"net/http/cgi"
 	"os"
 	"strings"
-	"sync"
 	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
@@ -14,16 +13,7 @@ import (
 	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
-func newArvadosClient() interface{} {
-	arv, err := arvadosclient.MakeArvadosClient()
-	if err != nil {
-		log.Println("MakeArvadosClient:", err)
-		return nil
-	}
-	return &arv
-}
-
-var connectionPool = &sync.Pool{New: newArvadosClient}
+var clientPool = arvadosclient.MakeClientPool()
 
 type authHandler struct {
 	handler *cgi.Handler
@@ -79,12 +69,12 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	repoName = pathParts[0]
 	repoName = strings.TrimRight(repoName, "/")
 
-	arv, ok := connectionPool.Get().(*arvadosclient.ArvadosClient)
-	if !ok || arv == nil {
-		statusCode, statusText = http.StatusInternalServerError, "connection pool failed"
+	arv := clientPool.Get()
+	if arv == nil {
+		statusCode, statusText = http.StatusInternalServerError, "connection pool failed: "+clientPool.Err().Error()
 		return
 	}
-	defer connectionPool.Put(arv)
+	defer clientPool.Put(arv)
 
 	// Ask API server whether the repository is readable using
 	// this token (by trying to read it!)

commit e14e011f667d314e557c580de69a271534b6149f
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Jun 12 01:53:05 2015 -0400

    5824: Move quoted-logging function to SDK.

diff --git a/sdk/go/httpserver/log.go b/sdk/go/httpserver/log.go
new file mode 100644
index 0000000..7bee887
--- /dev/null
+++ b/sdk/go/httpserver/log.go
@@ -0,0 +1,23 @@
+package httpserver
+
+import (
+	"log"
+	"strings"
+)
+
+var escaper = strings.NewReplacer("\"", "\\\"", "\\", "\\\\", "\n", "\\n")
+
+// Log calls log.Println but first transforms strings so they are
+// safer to write in logs (e.g., 'foo"bar' becomes
+// '"foo\"bar"'). Non-string args are left alone.
+func Log(args ...interface{}) {
+	newargs := make([]interface{}, len(args))
+	for i, arg := range args {
+		if s, ok := arg.(string); ok {
+			newargs[i] = "\"" + escaper.Replace(s) + "\""
+		} else {
+			newargs[i] = arg
+		}
+	}
+	log.Println(newargs...)
+}
diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index ff1a1d5..741e543 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -57,7 +57,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 			passwordToLog = apiToken[0:10]
 		}
 
-		log.Println(quoteStrings(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)...)
+		httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
 	}()
 
 	creds := auth.NewCredentialsFromHTTPRequest(r)
@@ -164,16 +164,3 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	handlerCopy.Env = append(handlerCopy.Env, "REMOTE_USER="+r.RemoteAddr) // Should be username
 	handlerCopy.ServeHTTP(&w, r)
 }
-
-var escaper = strings.NewReplacer("\"", "\\\"", "\\", "\\\\", "\n", "\\n")
-
-// Transform strings so they are safer to write in logs (e.g.,
-// 'foo"bar' becomes '"foo\"bar"'). Non-string args are left alone.
-func quoteStrings(args ...interface{}) []interface{} {
-	for i, arg := range args {
-		if s, ok := arg.(string); ok {
-			args[i] = "\"" + escaper.Replace(s) + "\""
-		}
-	}
-	return args
-}

commit 2dfb886e960cf918e54b5f03477f464afb322a9b
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Jun 17 01:57:33 2015 -0400

    5824: gofmt

diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index 685e241..ff1a1d5 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -9,8 +9,8 @@ import (
 	"sync"
 	"time"
 
-	"git.curoverse.com/arvados.git/sdk/go/auth"
 	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
+	"git.curoverse.com/arvados.git/sdk/go/auth"
 	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
diff --git a/services/arv-git-httpd/server.go b/services/arv-git-httpd/server.go
index 9e80481..c3c36da 100644
--- a/services/arv-git-httpd/server.go
+++ b/services/arv-git-httpd/server.go
@@ -3,6 +3,7 @@ package main
 import (
 	"net/http"
 	"net/http/cgi"
+
 	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 

commit 173ebc1102e6a5a5c3a26c1bb231a4b035713369
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Jun 12 01:39:27 2015 -0400

    5824: Move spying ResponseWriter to SDK.

diff --git a/sdk/go/httpserver/responsewriter.go b/sdk/go/httpserver/responsewriter.go
new file mode 100644
index 0000000..1af4dc8
--- /dev/null
+++ b/sdk/go/httpserver/responsewriter.go
@@ -0,0 +1,43 @@
+package httpserver
+
+import (
+	"net/http"
+)
+
+// ResponseWriter wraps http.ResponseWriter and exposes the status
+// sent, the number of bytes sent to the client, and the last write
+// error.
+type ResponseWriter struct {
+	http.ResponseWriter
+	wroteStatus *int	// Last status given to WriteHeader()
+	wroteBodyBytes *int	// Bytes successfully written
+	err *error		// Last error returned from Write()
+}
+
+func WrapResponseWriter(orig http.ResponseWriter) ResponseWriter {
+	return ResponseWriter{orig, new(int), new(int), new(error)}
+}
+
+func (w ResponseWriter) WriteHeader(s int) {
+	*w.wroteStatus = s
+	w.ResponseWriter.WriteHeader(s)
+}
+
+func (w ResponseWriter) Write(data []byte) (n int, err error) {
+	n, err = w.ResponseWriter.Write(data)
+	*w.wroteBodyBytes += n
+	*w.err = err
+	return
+}
+
+func (w ResponseWriter) WroteStatus() int {
+	return *w.wroteStatus
+}
+
+func (w ResponseWriter) WroteBodyBytes() int {
+	return *w.wroteBodyBytes
+}
+
+func (w ResponseWriter) Err() error {
+	return *w.err
+}
diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index ebda2a4..685e241 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -11,6 +11,7 @@ import (
 
 	"git.curoverse.com/arvados.git/sdk/go/auth"
 	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
 func newArvadosClient() interface{} {
@@ -24,16 +25,6 @@ func newArvadosClient() interface{} {
 
 var connectionPool = &sync.Pool{New: newArvadosClient}
 
-type spyingResponseWriter struct {
-	http.ResponseWriter
-	wroteStatus *int
-}
-
-func (w spyingResponseWriter) WriteHeader(s int) {
-	*w.wroteStatus = s
-	w.ResponseWriter.WriteHeader(s)
-}
-
 type authHandler struct {
 	handler *cgi.Handler
 }
@@ -43,13 +34,12 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	var statusText string
 	var apiToken string
 	var repoName string
-	var wroteStatus int
 	var validApiToken bool
 
-	w := spyingResponseWriter{wOrig, &wroteStatus}
+	w := httpserver.WrapResponseWriter(wOrig)
 
 	defer func() {
-		if wroteStatus == 0 {
+		if w.WroteStatus() == 0 {
 			// Nobody has called WriteHeader yet: that
 			// must be our job.
 			w.WriteHeader(statusCode)
@@ -67,7 +57,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 			passwordToLog = apiToken[0:10]
 		}
 
-		log.Println(quoteStrings(r.RemoteAddr, passwordToLog, wroteStatus, statusText, repoName, r.Method, r.URL.Path)...)
+		log.Println(quoteStrings(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)...)
 	}()
 
 	creds := auth.NewCredentialsFromHTTPRequest(r)

commit e889ec14bbd18cf82acfabc681d0db967772692d
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Jun 12 01:01:52 2015 -0400

    5824: Move HTTP server code to SDK.

diff --git a/services/arv-git-httpd/server.go b/sdk/go/httpserver/httpserver.go
similarity index 56%
copy from services/arv-git-httpd/server.go
copy to sdk/go/httpserver/httpserver.go
index 716b276..396fe42 100644
--- a/services/arv-git-httpd/server.go
+++ b/sdk/go/httpserver/httpserver.go
@@ -1,14 +1,13 @@
-package main
+package httpserver
 
 import (
 	"net"
 	"net/http"
-	"net/http/cgi"
 	"sync"
 	"time"
 )
 
-type server struct {
+type Server struct {
 	http.Server
 	Addr     string // host:port where the server is listening.
 	err      error
@@ -18,28 +17,15 @@ type server struct {
 	wantDown bool
 }
 
-func (srv *server) Start() error {
-	gitHandler := &cgi.Handler{
-		Path: theConfig.GitCommand,
-		Dir:  theConfig.Root,
-		Env: []string{
-			"GIT_PROJECT_ROOT=" + theConfig.Root,
-			"GIT_HTTP_EXPORT_ALL=",
-		},
-		InheritEnv: []string{"PATH"},
-		Args:       []string{"http-backend"},
-	}
-
-	// The rest of the work here is essentially
-	// http.ListenAndServe() with two more features: (1) whoever
-	// called Start() can discover which address:port we end up
-	// listening to -- which makes listening on ":0" useful in
-	// test suites -- and (2) the server can be shut down without
-	// killing the process -- which is useful in test cases, and
-	// makes it possible to shut down gracefully on SIGTERM
-	// without killing active connections.
-
-	addr, err := net.ResolveTCPAddr("tcp", theConfig.Addr)
+// Start is essentially (*http.Server)ListenAndServe() with two more
+// features: (1) by the time Start() returns, Addr is changed to the
+// address:port we ended up listening to -- which makes listening on
+// ":0" useful in test suites -- and (2) the server can be shut down
+// without killing the process -- which is useful in test cases, and
+// makes it possible to shut down gracefully on SIGTERM without
+// killing active connections.
+func (srv *Server) Start() error {
+	addr, err := net.ResolveTCPAddr("tcp", srv.Addr)
 	if err != nil {
 		return err
 	}
@@ -48,9 +34,6 @@ func (srv *server) Start() error {
 		return err
 	}
 	srv.Addr = srv.listener.Addr().String()
-	mux := http.NewServeMux()
-	mux.Handle("/", &authHandler{gitHandler})
-	srv.Handler = mux
 
 	mutex := &sync.RWMutex{}
 	srv.cond = sync.NewCond(mutex.RLocker())
@@ -68,8 +51,15 @@ func (srv *server) Start() error {
 	return nil
 }
 
+// Close shuts down the server and returns when it has stopped.
+func (srv *Server) Close() error {
+	srv.wantDown = true
+	srv.listener.Close()
+	return srv.Wait()
+}
+
 // Wait returns when the server has shut down.
-func (srv *server) Wait() error {
+func (srv *Server) Wait() error {
 	if srv.cond == nil {
 		return nil
 	}
@@ -81,15 +71,7 @@ func (srv *server) Wait() error {
 	return srv.err
 }
 
-// Close shuts down the server and returns when it has stopped.
-func (srv *server) Close() error {
-	srv.wantDown = true
-	srv.listener.Close()
-	return srv.Wait()
-}
-
 // tcpKeepAliveListener is copied from net/http because not exported.
-//
 type tcpKeepAliveListener struct {
 	*net.TCPListener
 }
diff --git a/services/arv-git-httpd/server.go b/services/arv-git-httpd/server.go
index 716b276..9e80481 100644
--- a/services/arv-git-httpd/server.go
+++ b/services/arv-git-httpd/server.go
@@ -1,21 +1,13 @@
 package main
 
 import (
-	"net"
 	"net/http"
 	"net/http/cgi"
-	"sync"
-	"time"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
 type server struct {
-	http.Server
-	Addr     string // host:port where the server is listening.
-	err      error
-	cond     *sync.Cond
-	running  bool
-	listener *net.TCPListener
-	wantDown bool
+	httpserver.Server
 }
 
 func (srv *server) Start() error {
@@ -29,77 +21,9 @@ func (srv *server) Start() error {
 		InheritEnv: []string{"PATH"},
 		Args:       []string{"http-backend"},
 	}
-
-	// The rest of the work here is essentially
-	// http.ListenAndServe() with two more features: (1) whoever
-	// called Start() can discover which address:port we end up
-	// listening to -- which makes listening on ":0" useful in
-	// test suites -- and (2) the server can be shut down without
-	// killing the process -- which is useful in test cases, and
-	// makes it possible to shut down gracefully on SIGTERM
-	// without killing active connections.
-
-	addr, err := net.ResolveTCPAddr("tcp", theConfig.Addr)
-	if err != nil {
-		return err
-	}
-	srv.listener, err = net.ListenTCP("tcp", addr)
-	if err != nil {
-		return err
-	}
-	srv.Addr = srv.listener.Addr().String()
 	mux := http.NewServeMux()
 	mux.Handle("/", &authHandler{gitHandler})
 	srv.Handler = mux
-
-	mutex := &sync.RWMutex{}
-	srv.cond = sync.NewCond(mutex.RLocker())
-	srv.running = true
-	go func() {
-		err = srv.Serve(tcpKeepAliveListener{srv.listener})
-		if !srv.wantDown {
-			srv.err = err
-		}
-		mutex.Lock()
-		srv.running = false
-		srv.cond.Broadcast()
-		mutex.Unlock()
-	}()
-	return nil
-}
-
-// Wait returns when the server has shut down.
-func (srv *server) Wait() error {
-	if srv.cond == nil {
-		return nil
-	}
-	srv.cond.L.Lock()
-	defer srv.cond.L.Unlock()
-	for srv.running {
-		srv.cond.Wait()
-	}
-	return srv.err
-}
-
-// Close shuts down the server and returns when it has stopped.
-func (srv *server) Close() error {
-	srv.wantDown = true
-	srv.listener.Close()
-	return srv.Wait()
-}
-
-// tcpKeepAliveListener is copied from net/http because not exported.
-//
-type tcpKeepAliveListener struct {
-	*net.TCPListener
-}
-
-func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
-	tc, err := ln.AcceptTCP()
-	if err != nil {
-		return
-	}
-	tc.SetKeepAlive(true)
-	tc.SetKeepAlivePeriod(3 * time.Minute)
-	return tc, nil
+	srv.Addr = theConfig.Addr
+	return srv.Server.Start()
 }

commit d2e546749afaa1ff8fff8cb920b9a54d58154b76
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu May 21 00:33:56 2015 -0400

    5824: Move request auth code into an SDK package. Support more ways of passing tokens.

diff --git a/sdk/go/auth/auth.go b/sdk/go/auth/auth.go
new file mode 100644
index 0000000..4a719e9
--- /dev/null
+++ b/sdk/go/auth/auth.go
@@ -0,0 +1,61 @@
+package auth
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+type Credentials struct {
+	Tokens []string
+}
+
+func NewCredentials() *Credentials {
+	return &Credentials{Tokens: []string{}}
+}
+
+func NewCredentialsFromHTTPRequest(r *http.Request) *Credentials {
+	c := NewCredentials()
+	c.LoadTokensFromHTTPRequest(r)
+	return c
+}
+
+// LoadTokensFromHttpRequest loads all tokens it can find in the
+// headers and query string of an http query.
+func (a *Credentials) LoadTokensFromHTTPRequest(r *http.Request) {
+	// Load plain token from "Authorization: OAuth2 ..." header
+	// (typically used by smart API clients)
+	if toks := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(toks) == 2 && toks[0] == "OAuth2" {
+		a.Tokens = append(a.Tokens, toks[1])
+	}
+
+	// Load base64-encoded token from "Authorization: Basic ..."
+	// header (typically used by git via credential helper)
+	if _, password, ok := BasicAuth(r); ok {
+		a.Tokens = append(a.Tokens, password)
+	}
+
+	// Load tokens from query string. It's generally not a good
+	// idea to pass tokens around this way, but passing a narrowly
+	// scoped token is a reasonable way to implement "secret link
+	// to an object" in a generic way.
+	//
+	// ParseQuery always returns a non-nil map which might have
+	// valid parameters, even when a decoding error causes it to
+	// return a non-nil err. We ignore err; hopefully the caller
+	// will also need to parse the query string for
+	// application-specific purposes and will therefore
+	// find/report decoding errors in a suitable way.
+	qvalues, _ := url.ParseQuery(r.URL.RawQuery)
+	if val, ok := qvalues["api_token"]; ok {
+		a.Tokens = append(a.Tokens, val...)
+	}
+
+	// TODO: Load token from Rails session cookie (if Rails site
+	// secret is known)
+}
+
+// TODO: LoadTokensFromHttpRequestBody(). We can't assume in
+// LoadTokensFromHttpRequest() that [or how] we should read and parse
+// the request body. This has to be requested explicitly by the
+// application.
diff --git a/services/arv-git-httpd/basic_auth_go13.go b/sdk/go/auth/basic_auth_go13.go
similarity index 97%
rename from services/arv-git-httpd/basic_auth_go13.go
rename to sdk/go/auth/basic_auth_go13.go
index 087f2c8..c0fe5fc 100644
--- a/services/arv-git-httpd/basic_auth_go13.go
+++ b/sdk/go/auth/basic_auth_go13.go
@@ -1,6 +1,6 @@
 // +build !go1.4
 
-package main
+package auth
 
 import (
 	"encoding/base64"
diff --git a/services/arv-git-httpd/basic_auth_go14.go b/sdk/go/auth/basic_auth_go14.go
similarity index 91%
rename from services/arv-git-httpd/basic_auth_go14.go
rename to sdk/go/auth/basic_auth_go14.go
index 6a0079a..aeedb06 100644
--- a/services/arv-git-httpd/basic_auth_go14.go
+++ b/sdk/go/auth/basic_auth_go14.go
@@ -1,6 +1,6 @@
 // +build go1.4
 
-package main
+package auth
 
 import (
 	"net/http"
diff --git a/services/arv-git-httpd/basic_auth_test.go b/sdk/go/auth/basic_auth_test.go
similarity index 98%
rename from services/arv-git-httpd/basic_auth_test.go
rename to sdk/go/auth/basic_auth_test.go
index 2bd84dc..935f696 100644
--- a/services/arv-git-httpd/basic_auth_test.go
+++ b/sdk/go/auth/basic_auth_test.go
@@ -1,4 +1,4 @@
-package main
+package auth
 
 import (
 	"net/http"
diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index 1165354..ebda2a4 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -9,6 +9,7 @@ import (
 	"sync"
 	"time"
 
+	"git.curoverse.com/arvados.git/sdk/go/auth"
 	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
 )
 
@@ -40,7 +41,7 @@ type authHandler struct {
 func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	var statusCode int
 	var statusText string
-	var username, password string
+	var apiToken string
 	var repoName string
 	var wroteStatus int
 	var validApiToken bool
@@ -49,7 +50,8 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 
 	defer func() {
 		if wroteStatus == 0 {
-			// Nobody has called WriteHeader yet: that must be our job.
+			// Nobody has called WriteHeader yet: that
+			// must be our job.
 			w.WriteHeader(statusCode)
 			w.Write([]byte(statusText))
 		}
@@ -58,24 +60,23 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 		// Otherwise: log the string <invalid> if a password is given, else an empty string.
 		passwordToLog := ""
 		if !validApiToken {
-			if len(password) > 0 {
+			if len(apiToken) > 0 {
 				passwordToLog = "<invalid>"
 			}
 		} else {
-			passwordToLog = password[0:10]
+			passwordToLog = apiToken[0:10]
 		}
 
-		log.Println(quoteStrings(r.RemoteAddr, username, passwordToLog, wroteStatus, statusText, repoName, r.Method, r.URL.Path)...)
+		log.Println(quoteStrings(r.RemoteAddr, passwordToLog, wroteStatus, statusText, repoName, r.Method, r.URL.Path)...)
 	}()
 
-	// HTTP request username is logged, but unused. Password is an
-	// Arvados API token.
-	username, password, ok := BasicAuth(r)
-	if !ok || username == "" || password == "" {
+	creds := auth.NewCredentialsFromHTTPRequest(r)
+	if len(creds.Tokens) == 0 {
 		statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
 		w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
 		return
 	}
+	apiToken = creds.Tokens[0]
 
 	// Access to paths "/foo/bar.git/*" and "/foo/bar/.git/*" are
 	// protected by the permissions on the repository named
@@ -97,7 +98,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 
 	// Ask API server whether the repository is readable using
 	// this token (by trying to read it!)
-	arv.ApiToken = password
+	arv.ApiToken = apiToken
 	reposFound := arvadosclient.Dict{}
 	if err := arv.List("repositories", arvadosclient.Dict{
 		"filters": [][]string{{"name", "=", repoName}},

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list