display_value = link.name
elsif value_info[:link_name]
display_value = value_info[:link_name]
- elsif value_info[:selection_name]
- display_value = value_info[:selection_name]
+ elsif (sn = value_info[:selection_name]) && sn != ""
+ display_value = sn
end
end
if (attr == :components) and (subattr.size > 2)
# Clean up /arvados/v1/../../discovery/v1 to /discovery/v1
url.sub! '/arvados/v1/../../', '/'
+ anon_tokens = [Rails.configuration.anonymous_user_token].select { |x| x && include_anon_token }
+
query = {
'reader_tokens' => ((tokens[:reader_tokens] ||
Thread.current[:reader_tokens] ||
[]) +
- (include_anon_token ? [Rails.configuration.anonymous_user_token] : [])).to_json,
+ anon_tokens).to_json,
}
if !data.nil?
data.each do |k,v|
# Each pdh has more than one collection; however, we should get only one for each
assert collections.size == 2, 'Expected two objects in the preloaded collection hash'
assert collections[pdh1], 'Expected collections for the passed in pdh #{pdh1}'
- assert_equal collections[pdh1].size, 1, 'Expected one collection for the passed in pdh #{pdh1}'
+ assert_equal collections[pdh1].size, 1, "Expected one collection for the passed in pdh #{pdh1}"
assert collections[pdh2], 'Expected collections for the passed in pdh #{pdh2}'
- assert_equal collections[pdh2].size, 1, 'Expected one collection for the passed in pdh #{pdh2}'
+ assert_equal collections[pdh2].size, 1, "Expected one collection for the passed in pdh #{pdh2}"
end
test "requesting a nonexistent object returns 404" do
assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foobar\?" # locator on command
assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foo" # mount input1
assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/bar" # mount input2
- assert_includes @response.body, "href=\"\/collections/1fd08fc162a5c6413070a8bd0bffc818+150" # mount workflow
+ assert_includes @response.body, "href=\"\/collections/f9ddda46bb293b6847da984e3aa735db+290" # mount workflow
assert_includes @response.body, "href=\"#Log\""
assert_includes @response.body, "href=\"#Provenance\""
end
def create_and_run_pipeline_in_aproject in_aproject, template_name, collection_fixture, choose_file=false
# collection in aproject to be used as input
collection = api_fixture('collections', collection_fixture)
+ collection['name'] ||= '' # API response is "" even if fixture attr is null
# create a pipeline instance
find('.btn', text: 'Run a process').click
if collection_fixture == 'foo_collection_in_aproject'
first('span', text: 'foo_tag').click
- elsif collection['name']
+ elsif collection['name'] != ''
first('span', text: "#{collection['name']}").click
else
collection_uuid = collection['uuid']
lib/cli
lib/cmd
lib/controller
+lib/controller/federation
+lib/controller/railsproxy
+lib/controller/router
+lib/controller/rpc
lib/crunchstat
lib/cloud
lib/cloud/azure
)
declare -a gostuff
-gostuff=(
- cmd/arvados-client
- cmd/arvados-server
- lib/cli
- lib/cmd
- lib/controller
- lib/crunchstat
- lib/cloud
- lib/cloud/azure
- lib/cloud/ec2
- lib/cloud/cloudtest
- lib/config
- lib/dispatchcloud
- lib/dispatchcloud/container
- lib/dispatchcloud/scheduler
- lib/dispatchcloud/ssh_executor
- lib/dispatchcloud/worker
- lib/service
- sdk/go/arvados
- sdk/go/arvadosclient
- sdk/go/auth
- sdk/go/blockdigest
- sdk/go/dispatch
- sdk/go/health
- sdk/go/httpserver
- sdk/go/manifest
- sdk/go/asyncbuf
- sdk/go/crunchrunner
- sdk/go/stats
- services/arv-git-httpd
- services/crunchstat
- services/health
- services/keep-web
- services/keepstore
- sdk/go/keepclient
- services/keep-balance
- services/keepproxy
- services/crunch-dispatch-local
- services/crunch-dispatch-slurm
- services/crunch-run
- services/ws
- tools/keep-block-check
- tools/keep-exercise
- tools/keep-rsync
- tools/sync-groups
-)
+gostuff=($(git grep -lw func | grep \\.go | sed -e 's/\/[^\/]*$//' | sort -u))
install_apps/workbench() {
cd "$WORKSPACE/apps/workbench" \
# assume emacs, or something, is offering a history buffer
# and pre-populating the command will only cause trouble
nextcmd=
- elif [[ "$nextcmd" != "install deps" ]]; then
- :
- elif [[ -e "$VENVDIR/bin/activate" ]]; then
- nextcmd="test lib/cmd"
- else
+ elif [[ ! -e "$VENVDIR/bin/activate" ]]; then
nextcmd="install deps"
+ else
+ nextcmd=""
fi
}
echo
help_interactive
nextcmd="install deps"
setnextcmd
- while read -p 'What next? ' -e -i "${nextcmd}" nextcmd; do
+ HISTFILE="$WORKSPACE/tmp/.history"
+ history -r
+ while read -p 'What next? ' -e -i "$nextcmd" nextcmd; do
+ history -s "$nextcmd"
+ history -w
read verb target opts <<<"${nextcmd}"
target="${target%/}"
target="${target/\/:/:}"
os.Stdout, os.Stderr = stdout, stderr
for i, tmpfile := range tmpfiles {
- c.Log("checking %s", i)
+ c.Logf("checking %s", i)
_, err := tmpfile.Seek(0, io.SeekStart)
c.Assert(err, check.IsNil)
leaked, err := ioutil.ReadAll(tmpfile)
# Workbench2 configs
VocabularyURL: ""
FileViewersConfigURL: ""
+
+ # Use experimental controller code (see https://dev.arvados.org/issues/14287)
+ EnableBetaController14287: false
"Containers.StaleLockTimeout": false,
"Containers.SupportedDockerImageFormats": true,
"Containers.UsePreemptibleInstances": true,
+ "EnableBetaController14287": false,
"Git": false,
"InstanceTypes": true,
"InstanceTypes.*": true,
# Workbench2 configs
VocabularyURL: ""
FileViewersConfigURL: ""
+
+ # Use experimental controller code (see https://dev.arvados.org/issues/14287)
+ EnableBetaController14287: false
`)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+ "context"
+ "crypto/md5"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+ "git.curoverse.com/arvados.git/lib/controller/rpc"
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+)
+
+type Conn struct {
+ cluster *arvados.Cluster
+ local backend
+ remotes map[string]backend
+}
+
+func New(cluster *arvados.Cluster) arvados.API {
+ local := railsproxy.NewConn(cluster)
+ remotes := map[string]backend{}
+ for id, remote := range cluster.RemoteClusters {
+ if !remote.Proxy {
+ continue
+ }
+ remotes[id] = rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id))
+ }
+
+ return &Conn{
+ cluster: cluster,
+ local: local,
+ remotes: remotes,
+ }
+}
+
+// Return a new rpc.TokenProvider that takes the client-provided
+// tokens from an incoming request context, determines whether they
+// should (and can) be salted for the given remoteID, and returns the
+// resulting tokens.
+func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider {
+ return func(ctx context.Context) ([]string, error) {
+ var tokens []string
+ incoming, ok := auth.FromContext(ctx)
+ if !ok {
+ return nil, errors.New("no token provided")
+ }
+ for _, token := range incoming.Tokens {
+ salted, err := auth.SaltToken(token, remoteID)
+ switch err {
+ case nil:
+ tokens = append(tokens, salted)
+ case auth.ErrSalted:
+ tokens = append(tokens, token)
+ case auth.ErrObsoleteToken:
+ ctx := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{token}})
+ aca, err := local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{})
+ if errStatus(err) == http.StatusUnauthorized {
+ // pass through unmodified
+ tokens = append(tokens, token)
+ continue
+ } else if err != nil {
+ return nil, err
+ }
+ salted, err := auth.SaltToken(aca.TokenV2(), remoteID)
+ if err != nil {
+ return nil, err
+ }
+ tokens = append(tokens, salted)
+ default:
+ return nil, err
+ }
+ }
+ return tokens, nil
+ }
+}
+
+// Return suitable backend for a query about the given cluster ID
+// ("aaaaa") or object UUID ("aaaaa-dz642-abcdefghijklmno").
+func (conn *Conn) chooseBackend(id string) backend {
+ if len(id) == 27 {
+ id = id[:5]
+ } else if len(id) != 5 {
+ // PDH or bogus ID
+ return conn.local
+ }
+ if id == conn.cluster.ClusterID {
+ return conn.local
+ } else if be, ok := conn.remotes[id]; ok {
+ return be
+ } else {
+ // TODO: return an "always error" backend?
+ return conn.local
+ }
+}
+
+// Call fn with the local backend; then, if fn returned 404, call fn
+// on the available remote backends (possibly concurrently) until one
+// succeeds.
+//
+// The second argument to fn is the cluster ID of the remote backend,
+// or "" for the local backend.
+//
+// A non-nil error means all backends failed.
+func (conn *Conn) tryLocalThenRemotes(ctx context.Context, fn func(context.Context, string, backend) error) error {
+ if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound {
+ return err
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ errchan := make(chan error, len(conn.remotes))
+ for remoteID, be := range conn.remotes {
+ remoteID, be := remoteID, be
+ go func() {
+ errchan <- fn(ctx, remoteID, be)
+ }()
+ }
+ all404 := true
+ var errs []error
+ for i := 0; i < cap(errchan); i++ {
+ err := <-errchan
+ if err == nil {
+ return nil
+ }
+ all404 = all404 && errStatus(err) == http.StatusNotFound
+ errs = append(errs, err)
+ }
+ if all404 {
+ return notFoundError{}
+ }
+ // FIXME: choose appropriate HTTP status
+ return fmt.Errorf("errors: %v", errs)
+}
+
+func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+ return conn.chooseBackend(options.ClusterID).CollectionCreate(ctx, options)
+}
+
+func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+ return conn.chooseBackend(options.UUID).CollectionUpdate(ctx, options)
+}
+
+func rewriteManifest(mt, remoteID string) string {
+ return regexp.MustCompile(` [0-9a-f]{32}\+[^ ]*`).ReplaceAllStringFunc(mt, func(tok string) string {
+ return strings.Replace(tok, "+A", "+R"+remoteID+"-", -1)
+ })
+}
+
+// this could be in sdk/go/arvados
+func portableDataHash(mt string) string {
+ h := md5.New()
+ blkRe := regexp.MustCompile(`^ [0-9a-f]{32}\+\d+`)
+ size := 0
+ _ = regexp.MustCompile(` ?[^ ]*`).ReplaceAllFunc([]byte(mt), func(tok []byte) []byte {
+ if m := blkRe.Find(tok); m != nil {
+ // write hash+size, ignore remaining block hints
+ tok = m
+ }
+ n, err := h.Write(tok)
+ if err != nil {
+ panic(err)
+ }
+ size += n
+ return nil
+ })
+ return fmt.Sprintf("%x+%d", h.Sum(nil), size)
+}
+
+func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+ if len(options.UUID) == 27 {
+ // UUID is really a UUID
+ c, err := conn.chooseBackend(options.UUID).CollectionGet(ctx, options)
+ if err == nil && options.UUID[:5] != conn.cluster.ClusterID {
+ c.ManifestText = rewriteManifest(c.ManifestText, options.UUID[:5])
+ }
+ return c, err
+ } else {
+ // UUID is a PDH
+ first := make(chan arvados.Collection, 1)
+ err := conn.tryLocalThenRemotes(ctx, func(ctx context.Context, remoteID string, be backend) error {
+ c, err := be.CollectionGet(ctx, options)
+ if err != nil {
+ return err
+ }
+ // options.UUID is either hash+size or
+ // hash+size+hints; only hash+size need to
+ // match the computed PDH.
+ if pdh := portableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
+ ctxlog.FromContext(ctx).Warnf("bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID)
+ return notFoundError{}
+ }
+ if remoteID != "" {
+ c.ManifestText = rewriteManifest(c.ManifestText, remoteID)
+ }
+ select {
+ case first <- c:
+ return nil
+ default:
+ // lost race, return value doesn't matter
+ return nil
+ }
+ })
+ if err != nil {
+ return arvados.Collection{}, err
+ }
+ return <-first, nil
+ }
+}
+
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+ return conn.local.CollectionList(ctx, options)
+}
+
+func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options)
+}
+
+func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ return conn.chooseBackend(options.UUID).CollectionUsedBy(ctx, options)
+}
+
+func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ return conn.chooseBackend(options.UUID).CollectionDelete(ctx, options)
+}
+
+func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ return conn.chooseBackend(options.UUID).CollectionTrash(ctx, options)
+}
+
+func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+ return conn.chooseBackend(options.UUID).CollectionUntrash(ctx, options)
+}
+
+func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.ClusterID).ContainerCreate(ctx, options)
+}
+
+func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.UUID).ContainerUpdate(ctx, options)
+}
+
+func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.UUID).ContainerGet(ctx, options)
+}
+
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+ return conn.local.ContainerList(ctx, options)
+}
+
+func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.UUID).ContainerDelete(ctx, options)
+}
+
+func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.UUID).ContainerLock(ctx, options)
+}
+
+func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
+}
+
+func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+ return conn.chooseBackend(options.ClusterID).SpecimenCreate(ctx, options)
+}
+
+func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+ return conn.chooseBackend(options.UUID).SpecimenUpdate(ctx, options)
+}
+
+func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+ return conn.chooseBackend(options.UUID).SpecimenGet(ctx, options)
+}
+
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+ return conn.local.SpecimenList(ctx, options)
+}
+
+func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+ return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+ return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
+}
+
+type backend interface{ arvados.API }
+
+type notFoundError struct{}
+
+func (notFoundError) HTTPStatus() int { return http.StatusNotFound }
+func (notFoundError) Error() string { return "not found" }
+
+func errStatus(err error) int {
+ if httpErr, ok := err.(interface{ HTTPStatus() int }); ok {
+ return httpErr.HTTPStatus()
+ } else {
+ return http.StatusInternalServerError
+ }
+}
import (
"bytes"
+ "context"
"encoding/json"
"fmt"
"io"
// provided by the integration test environment.
remoteServer *httpserver.Server
// remoteMock ("zmock") appends each incoming request to
- // remoteMockRequests, and returns an empty 200 response.
+ // remoteMockRequests, and returns 200 with an empty JSON
+ // object.
remoteMock *httpserver.Server
remoteMockRequests []http.Request
}
c.Assert(s.remoteMock.Start(), check.IsNil)
cluster := &arvados.Cluster{
- ClusterID: "zhome",
- PostgreSQL: integrationTestCluster().PostgreSQL,
+ ClusterID: "zhome",
+ PostgreSQL: integrationTestCluster().PostgreSQL,
+ EnableBetaController14287: enableBetaController14287,
}
cluster.TLS.Insecure = true
cluster.API.MaxItemsPerResponse = 1000
arvadostest.SetServiceURL(&cluster.Services.Controller, "http://localhost:/")
s.testHandler = &Handler{Cluster: cluster}
s.testServer = newServerFromIntegrationTestEnv(c)
- s.testServer.Server.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(s.log, s.testHandler))
+ s.testServer.Server.Handler = httpserver.HandlerWithContext(
+ ctxlog.Context(context.Background(), s.log),
+ httpserver.AddRequestIDs(httpserver.LogRequests(s.testHandler)))
cluster.RemoteClusters = map[string]arvados.RemoteCluster{
"zzzzz": {
req.Body.Close()
req.Body = ioutil.NopCloser(b)
s.remoteMockRequests = append(s.remoteMockRequests, *req)
+ // Repond 200 with a valid JSON object
+ fmt.Fprint(w, "{}")
}
func (s *FederationSuite) TearDownTest(c *check.C) {
}
}
-func (s *FederationSuite) testRequest(req *http.Request) *http.Response {
+func (s *FederationSuite) testRequest(req *http.Request) *httptest.ResponseRecorder {
resp := httptest.NewRecorder()
s.testServer.Server.Handler.ServeHTTP(resp, req)
- return resp.Result()
+ return resp
}
func (s *FederationSuite) TestLocalRequest(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zhome-", 1), nil)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
s.checkHandledLocally(c, resp)
}
func (s *FederationSuite) TestNoAuth(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
s.checkJSONErrorMatches(c, resp, `Not logged in`)
}
func (s *FederationSuite) TestBadAuth(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
s.checkJSONErrorMatches(c, resp, `Not logged in`)
}
func (s *FederationSuite) TestNoAccess(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.SpectatorToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
s.checkJSONErrorMatches(c, resp, `.*not found`)
}
func (s *FederationSuite) TestGetUnknownRemote(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zz404-", 1), nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
s.checkJSONErrorMatches(c, resp, `.*no proxy available for cluster zz404`)
}
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadGateway)
s.checkJSONErrorMatches(c, resp, `.*HTTP response to HTTPS client`)
}
func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var wf arvados.Workflow
c.Check(json.NewDecoder(resp.Body).Decode(&wf), check.IsNil)
func (s *FederationSuite) TestOptionsMethod(c *check.C) {
req := httptest.NewRequest("OPTIONS", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
req.Header.Set("Origin", "https://example.com")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
body, err := ioutil.ReadAll(resp.Body)
c.Check(err, check.IsNil)
func (s *FederationSuite) TestRemoteWithTokenInQuery(c *check.C) {
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1)+"?api_token="+arvadostest.ActiveToken, nil)
- s.testRequest(req)
+ s.testRequest(req).Result()
c.Assert(s.remoteMockRequests, check.HasLen, 1)
pr := s.remoteMockRequests[0]
// Token is salted and moved from query to Authorization header.
}
func (s *FederationSuite) TestLocalTokenSalted(c *check.C) {
- req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1), nil)
- req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- s.testRequest(req)
- c.Assert(s.remoteMockRequests, check.HasLen, 1)
- pr := s.remoteMockRequests[0]
- // The salted token here has a "zzzzz-" UUID instead of a
- // "ztest-" UUID because ztest's local database has the
- // "zzzzz-" test fixtures. The "secret" part is HMAC(sha1,
- // arvadostest.ActiveToken, "zmock") = "7fd3...".
- c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/7fd31b61f39c0e82a4155592163218272cedacdc")
+ defer s.localServiceReturns404(c).Close()
+ for _, path := range []string{
+ // During the transition to the strongly typed
+ // controller implementation (#14287), workflows and
+ // collections test different code paths.
+ "/arvados/v1/workflows/" + strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1),
+ "/arvados/v1/collections/" + strings.Replace(arvadostest.UserAgreementCollection, "zzzzz-", "zmock-", 1),
+ } {
+ c.Log("testing path ", path)
+ s.remoteMockRequests = nil
+ req := httptest.NewRequest("GET", path, nil)
+ req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+ s.testRequest(req).Result()
+ c.Assert(s.remoteMockRequests, check.HasLen, 1)
+ pr := s.remoteMockRequests[0]
+ // The salted token here has a "zzzzz-" UUID instead of a
+ // "ztest-" UUID because ztest's local database has the
+ // "zzzzz-" test fixtures. The "secret" part is HMAC(sha1,
+ // arvadostest.ActiveToken, "zmock") = "7fd3...".
+ c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/7fd31b61f39c0e82a4155592163218272cedacdc")
+ }
}
func (s *FederationSuite) TestRemoteTokenNotSalted(c *check.C) {
+ defer s.localServiceReturns404(c).Close()
// remoteToken can be any v1 token that doesn't appear in
// ztest's local db.
remoteToken := "abcdef00000000000000000000000000000000000000000000"
- req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1), nil)
- req.Header.Set("Authorization", "Bearer "+remoteToken)
- s.testRequest(req)
- c.Assert(s.remoteMockRequests, check.HasLen, 1)
- pr := s.remoteMockRequests[0]
- c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer "+remoteToken)
+
+ for _, path := range []string{
+ // During the transition to the strongly typed
+ // controller implementation (#14287), workflows and
+ // collections test different code paths.
+ "/arvados/v1/workflows/" + strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1),
+ "/arvados/v1/collections/" + strings.Replace(arvadostest.UserAgreementCollection, "zzzzz-", "zmock-", 1),
+ } {
+ c.Log("testing path ", path)
+ s.remoteMockRequests = nil
+ req := httptest.NewRequest("GET", path, nil)
+ req.Header.Set("Authorization", "Bearer "+remoteToken)
+ s.testRequest(req).Result()
+ c.Assert(s.remoteMockRequests, check.HasLen, 1)
+ pr := s.remoteMockRequests[0]
+ c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer "+remoteToken)
+ }
}
func (s *FederationSuite) TestWorkflowCRUD(c *check.C) {
- wf := arvados.Workflow{
- Description: "TestCRUD",
- }
+ var wf arvados.Workflow
{
- body := &strings.Builder{}
- json.NewEncoder(body).Encode(&wf)
req := httptest.NewRequest("POST", "/arvados/v1/workflows", strings.NewReader(url.Values{
- "workflow": {body.String()},
+ "workflow": {`{"description": "TestCRUD"}`},
}.Encode()))
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req := httptest.NewRequest(method, "/arvados/v1/workflows/"+wf.UUID, strings.NewReader(form.Encode()))
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
s.checkResponseOK(c, resp)
err := json.NewDecoder(resp.Body).Decode(&wf)
c.Check(err, check.IsNil)
{
req := httptest.NewRequest("DELETE", "/arvados/v1/workflows/"+wf.UUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
s.checkResponseOK(c, resp)
err := json.NewDecoder(resp.Body).Decode(&wf)
c.Check(err, check.IsNil)
{
req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+wf.UUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
}
}
func (s *FederationSuite) localServiceReturns404(c *check.C) *httpserver.Server {
return s.localServiceHandler(c, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.WriteHeader(404)
+ if req.URL.Path == "/arvados/v1/api_client_authorizations/current" {
+ if req.Header.Get("Authorization") == "Bearer "+arvadostest.ActiveToken {
+ json.NewEncoder(w).Encode(arvados.APIClientAuthorization{UUID: arvadostest.ActiveTokenUUID, APIToken: arvadostest.ActiveToken})
+ } else {
+ w.WriteHeader(http.StatusUnauthorized)
+ }
+ } else {
+ w.WriteHeader(404)
+ }
}))
}
req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementCollection, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var col arvados.Collection
}).Encode()))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
- resp = s.testRequest(req)
+ resp = s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
col = arvados.Collection{}
req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementCollection, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var col arvados.Collection
c.Check(json.NewDecoder(resp.Body).Decode(&col), check.IsNil)
req := httptest.NewRequest("GET", "/arvados/v1/collections/zzzzz-4zz18-fakefakefakefak", nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
}
req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var col arvados.Collection
req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
defer resp.Body.Close()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
defer resp.Body.Close()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
req.Header.Set("Authorization", "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/282d7d172b6cfdce364c5ed12ddf7417b2d00065")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var col arvados.Collection
req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
req.Header.Set("Authorization", "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/282d7d172b6cfdce364c5ed12ddf7417b2d00065")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
}
defer s.localServiceReturns404(c).Close()
req := httptest.NewRequest("GET", "/arvados/v1/container_requests/"+arvadostest.QueuedContainerRequestUUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr arvados.ContainerRequest
c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
strings.NewReader(fmt.Sprintf(`{"container_request": {"priority": %d}}`, pri)))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req.Header.Set("Content-type", "application/json")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr arvados.ContainerRequest
c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
`))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req.Header.Set("Content-type", "application/json")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr arvados.ContainerRequest
c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
arvadostest.SetServiceURL(&s.testHandler.Cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
s.testHandler.Cluster.ClusterID = "zzzzz"
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr struct {
arvados.ContainerRequest `json:"container_request"`
`))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req.Header.Set("Content-type", "application/json")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr struct {
arvados.ContainerRequest `json:"container_request"`
`))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2+"/zzzzz-dz642-parentcontainer")
req.Header.Set("Content-type", "application/json")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cr struct {
arvados.ContainerRequest `json:"container_request"`
`))
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
req.Header.Set("Content-type", "application/json")
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
}
req := httptest.NewRequest("GET", "/arvados/v1/containers/"+arvadostest.QueuedContainerUUID, nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
resp := s.testRequest(req)
- c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+ c.Check(resp.Code, check.Equals, http.StatusOK)
var cn arvados.Container
c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
c.Check(cn.UUID, check.Equals, arvadostest.QueuedContainerUUID)
req := httptest.NewRequest("GET", "/arvados/v1/containers?count=none&filters="+
url.QueryEscape(fmt.Sprintf(`[["uuid", "in", ["%v"]]]`, arvadostest.QueuedContainerUUID)), nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cn arvados.ContainerList
c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
+ c.Assert(cn.Items, check.HasLen, 1)
c.Check(cn.Items[0].UUID, check.Equals, arvadostest.QueuedContainerUUID)
}
url.QueryEscape(`["uuid", "command"]`)),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
var cn arvados.ContainerList
c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
url.QueryEscape(`["uuid", "command"]`)),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadGateway)
s.checkJSONErrorMatches(c, resp, `error fetching from zhome \(404 Not Found\): EOF`)
}
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
c.Check(callCount, check.Equals, 2)
var cn arvados.ContainerList
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
c.Check(callCount, check.Equals, 2)
var cn arvados.ContainerList
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
s.checkJSONErrorMatches(c, resp, `Federated multi-object request for 2 objects which is more than max page size 1.`)
}
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
}
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
}
arvadostest.QueuedContainerUUID))),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
}
url.QueryEscape(`["command"]`)),
nil)
req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
- resp := s.testRequest(req)
+ resp := s.testRequest(req).Result()
c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
s.checkJSONErrorMatches(c, resp, `Federated multi-object request must include 'uuid' in 'select'`)
}
"time"
"git.curoverse.com/arvados.git/lib/config"
+ "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+ "git.curoverse.com/arvados.git/lib/controller/router"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
func (h *Handler) CheckHealth() error {
h.setupOnce.Do(h.setup)
- _, _, err := findRailsAPI(h.Cluster)
+ _, _, err := railsproxy.FindRailsAPI(h.Cluster)
return err
}
io.Copy(w, &buf)
}))
+ if h.Cluster.EnableBetaController14287 {
+ rtr := router.New(h.Cluster)
+ mux.Handle("/arvados/v1/collections", rtr)
+ mux.Handle("/arvados/v1/collections/", rtr)
+ }
+
hs := http.NotFoundHandler()
hs = prepend(hs, h.proxyRailsAPI)
hs = h.setupProxyRemoteCluster(hs)
}
func (h *Handler) localClusterRequest(req *http.Request) (*http.Response, error) {
- urlOut, insecure, err := findRailsAPI(h.Cluster)
+ urlOut, insecure, err := railsproxy.FindRailsAPI(h.Cluster)
if err != nil {
return nil, err
}
check "gopkg.in/check.v1"
)
+var enableBetaController14287 bool
+
// Gocheck boilerplate
func Test(t *testing.T) {
- check.TestingT(t)
+ for _, enableBetaController14287 = range []bool{false, true} {
+ check.TestingT(t)
+ }
}
var _ = check.Suite(&HandlerSuite{})
s.cluster = &arvados.Cluster{
ClusterID: "zzzzz",
PostgreSQL: integrationTestCluster().PostgreSQL,
+
+ EnableBetaController14287: enableBetaController14287,
}
s.cluster.TLS.Insecure = true
arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
return h.Message
}
-// headers that shouldn't be forwarded when proxying. See
-// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
var dropHeaders = map[string]bool{
+ // Headers that shouldn't be forwarded when proxying. See
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
"Connection": true,
"Keep-Alive": true,
"Proxy-Authenticate": true,
"Proxy-Authorization": true,
- // this line makes gofmt 1.10 and 1.11 agree
- "TE": true,
- "Trailer": true,
- "Transfer-Encoding": true, // *-Encoding headers interfer with Go's automatic compression/decompression
- "Content-Encoding": true,
+ // (comment/space here makes gofmt1.10 agree with gofmt1.11)
+ "TE": true,
+ "Trailer": true,
+ "Upgrade": true,
+
+ // Headers that would interfere with Go's automatic
+ // compression/decompression if we forwarded them.
"Accept-Encoding": true,
- "Upgrade": true,
+ "Content-Encoding": true,
+ "Transfer-Encoding": true,
}
type ResponseFilter func(*http.Response, error) (*http.Response, error)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Package railsproxy implements Arvados APIs by proxying to the
+// RailsAPI server on the local machine.
+package railsproxy
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "git.curoverse.com/arvados.git/lib/controller/rpc"
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
+)
+
+// For now, FindRailsAPI always uses the rails API running on this
+// node.
+func FindRailsAPI(cluster *arvados.Cluster) (*url.URL, bool, error) {
+ var best *url.URL
+ for target := range cluster.Services.RailsAPI.InternalURLs {
+ target := url.URL(target)
+ best = &target
+ if strings.HasPrefix(target.Host, "localhost:") || strings.HasPrefix(target.Host, "127.0.0.1:") || strings.HasPrefix(target.Host, "[::1]:") {
+ break
+ }
+ }
+ if best == nil {
+ return nil, false, fmt.Errorf("Services.RailsAPI.InternalURLs is empty")
+ }
+ return best, cluster.TLS.Insecure, nil
+}
+
+func NewConn(cluster *arvados.Cluster) *rpc.Conn {
+ url, insecure, err := FindRailsAPI(cluster)
+ if err != nil {
+ panic(err)
+ }
+ return rpc.NewConn(cluster.ClusterID, url, insecure, provideIncomingToken)
+}
+
+func provideIncomingToken(ctx context.Context) ([]string, error) {
+ incoming, ok := auth.FromContext(ctx)
+ if !ok {
+ return nil, errors.New("no token provided")
+ }
+ return incoming.Tokens, nil
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "reflect"
+ "runtime"
+
+ check "gopkg.in/check.v1"
+)
+
+// a Gocheck checker for testing the name of a function. Used with
+// (*arvadostest.APIStub).Calls() to check that an HTTP request has
+// been routed to the correct arvados.API method.
+//
+// c.Check(bytes.NewBuffer().Read, isMethodNamed, "Read")
+var isMethodNamed check.Checker = &chkIsMethodNamed{
+ CheckerInfo: &check.CheckerInfo{
+ Name: "isMethodNamed",
+ Params: []string{"obtained", "expected"},
+ },
+}
+
+type chkIsMethodNamed struct{ *check.CheckerInfo }
+
+func (*chkIsMethodNamed) Check(params []interface{}, names []string) (bool, string) {
+ methodName := runtime.FuncForPC(reflect.ValueOf(params[0]).Pointer()).Name()
+ regex := `.*\)\.` + params[1].(string) + `(-.*)?`
+ return check.Matches.Check([]interface{}{methodName, regex}, names)
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+type errorWithStatus struct {
+ code int
+ error
+}
+
+func (err errorWithStatus) HTTPStatus() int {
+ return err.code
+}
+
+func httpError(code int, err error) error {
+ return errorWithStatus{code: code, error: err}
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+// Parse req as an Arvados V1 API request and return the request
+// parameters.
+//
+// If the request has a parameter whose name is attrsKey (e.g.,
+// "collection"), it is renamed to "attrs".
+func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[string]interface{}, error) {
+ err := req.ParseForm()
+ if err != nil {
+ return nil, httpError(http.StatusBadRequest, err)
+ }
+ params := map[string]interface{}{}
+
+ // Load parameters from req.Form, which (after
+ // req.ParseForm()) includes the query string and -- when
+ // Content-Type is application/x-www-form-urlencoded -- the
+ // request body.
+ for k, values := range req.Form {
+ // All of these form values arrive as strings, so we
+ // need some type-guessing to accept non-string
+ // inputs:
+ //
+ // Values for parameters that take ints (limit=1) or
+ // bools (include_trash=1) are parsed accordingly.
+ //
+ // "null" and "" are nil.
+ //
+ // Values that look like JSON objects, arrays, or
+ // strings are parsed as JSON.
+ //
+ // The rest are left as strings.
+ for _, v := range values {
+ switch {
+ case intParams[k]:
+ params[k], err = strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ case boolParams[k]:
+ params[k] = stringToBool(v)
+ case v == "null" || v == "":
+ params[k] = nil
+ case strings.HasPrefix(v, "["):
+ var j []interface{}
+ err := json.Unmarshal([]byte(v), &j)
+ if err != nil {
+ return nil, err
+ }
+ params[k] = j
+ case strings.HasPrefix(v, "{"):
+ var j map[string]interface{}
+ err := json.Unmarshal([]byte(v), &j)
+ if err != nil {
+ return nil, err
+ }
+ params[k] = j
+ case strings.HasPrefix(v, "\""):
+ var j string
+ err := json.Unmarshal([]byte(v), &j)
+ if err != nil {
+ return nil, err
+ }
+ params[k] = j
+ default:
+ params[k] = v
+ }
+ // TODO: Need to accept "?foo[]=bar&foo[]=baz"
+ // as foo=["bar","baz"]?
+ }
+ }
+
+ // Decode body as JSON if Content-Type request header is
+ // missing or application/json.
+ mt := req.Header.Get("Content-Type")
+ if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
+ return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
+ } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
+ jsonParams := map[string]interface{}{}
+ err := json.NewDecoder(req.Body).Decode(&jsonParams)
+ if err != nil {
+ return nil, httpError(http.StatusBadRequest, err)
+ }
+ for k, v := range jsonParams {
+ params[k] = v
+ }
+ if attrsKey != "" && params[attrsKey] == nil {
+ // Copy top-level parameters from JSON request
+ // body into params[attrsKey]. Some SDKs rely
+ // on this Rails API feature; see
+ // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
+ params[attrsKey] = jsonParams
+ }
+ }
+
+ routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
+ for _, p := range routeParams {
+ params[p.Key] = p.Value
+ }
+
+ if v, ok := params[attrsKey]; ok && attrsKey != "" {
+ params["attrs"] = v
+ delete(params, attrsKey)
+ }
+
+ if order, ok := params["order"].(string); ok {
+ // We must accept strings ("foo, bar desc") and arrays
+ // (["foo", "bar desc"]) because RailsAPI does.
+ // Convert to an array here before trying to unmarshal
+ // into options structs.
+ if order == "" {
+ delete(params, "order")
+ } else {
+ params["order"] = strings.Split(order, ",")
+ }
+ }
+
+ return params, nil
+}
+
+// Copy src to dst, using json as an intermediate format in order to
+// invoke src's json-marshaling and dst's json-unmarshaling behaviors.
+func (rtr *router) transcode(src interface{}, dst interface{}) error {
+ var errw error
+ pr, pw := io.Pipe()
+ go func() {
+ defer pw.Close()
+ errw = json.NewEncoder(pw).Encode(src)
+ }()
+ defer pr.Close()
+ err := json.NewDecoder(pr).Decode(dst)
+ if errw != nil {
+ return errw
+ }
+ return err
+}
+
+var intParams = map[string]bool{
+ "limit": true,
+ "offset": true,
+}
+
+var boolParams = map[string]bool{
+ "distinct": true,
+ "ensure_unique_name": true,
+ "include_trash": true,
+ "include_old_versions": true,
+}
+
+func stringToBool(s string) bool {
+ switch s {
+ case "", "false", "0":
+ return false
+ default:
+ return true
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+ check "gopkg.in/check.v1"
+)
+
+type testReq struct {
+ method string
+ path string
+ token string // default is ActiveTokenV2; use noToken to omit
+ param map[string]interface{}
+ attrs map[string]interface{}
+ attrsKey string
+ header http.Header
+
+ // variations on request formatting
+ json bool
+ jsonAttrsTop bool
+ jsonStringParam bool
+ tokenInBody bool
+ tokenInQuery bool
+ noContentType bool
+
+ body *bytes.Buffer
+}
+
+const noToken = "(no token)"
+
+func (tr *testReq) Request() *http.Request {
+ param := map[string]interface{}{}
+ for k, v := range tr.param {
+ param[k] = v
+ }
+
+ if tr.body != nil {
+ // caller provided a buffer
+ } else if tr.json {
+ if tr.jsonAttrsTop {
+ for k, v := range tr.attrs {
+ param[k] = v
+ }
+ } else if tr.attrs != nil {
+ param[tr.attrsKey] = tr.attrs
+ }
+ tr.body = bytes.NewBuffer(nil)
+ err := json.NewEncoder(tr.body).Encode(param)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ values := make(url.Values)
+ for k, v := range param {
+ if vs, ok := v.(string); ok && !tr.jsonStringParam {
+ values.Set(k, vs)
+ } else {
+ jv, err := json.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ values.Set(k, string(jv))
+ }
+ }
+ if tr.attrs != nil {
+ jattrs, err := json.Marshal(tr.attrs)
+ if err != nil {
+ panic(err)
+ }
+ values.Set(tr.attrsKey, string(jattrs))
+ }
+ tr.body = bytes.NewBuffer(nil)
+ io.WriteString(tr.body, values.Encode())
+ }
+ method := tr.method
+ if method == "" {
+ method = "GET"
+ }
+ path := tr.path
+ if path == "" {
+ path = "example/test/path"
+ }
+ req := httptest.NewRequest(method, "https://an.example/"+path, tr.body)
+ token := tr.token
+ if token == "" {
+ token = arvadostest.ActiveTokenV2
+ }
+ if token != noToken {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ if tr.json {
+ req.Header.Set("Content-Type", "application/json")
+ } else {
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ }
+ for k, v := range tr.header {
+ req.Header[k] = append([]string(nil), v...)
+ }
+ return req
+}
+
+func (tr *testReq) bodyContent() string {
+ return string(tr.body.Bytes())
+}
+
+func (s *RouterSuite) TestAttrsInBody(c *check.C) {
+ attrs := map[string]interface{}{"foo": "bar"}
+ for _, tr := range []testReq{
+ {attrsKey: "model_name", json: true, attrs: attrs},
+ {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true},
+ } {
+ c.Logf("tr: %#v", tr)
+ req := tr.Request()
+ params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+ c.Logf("params: %#v", params)
+ c.Assert(err, check.IsNil)
+ c.Check(params, check.NotNil)
+ c.Assert(params["attrs"], check.FitsTypeOf, map[string]interface{}{})
+ c.Check(params["attrs"].(map[string]interface{})["foo"], check.Equals, "bar")
+ }
+}
+
+func (s *RouterSuite) TestBoolParam(c *check.C) {
+ testKey := "ensure_unique_name"
+
+ for i, tr := range []testReq{
+ {method: "POST", param: map[string]interface{}{testKey: false}, json: true},
+ {method: "POST", param: map[string]interface{}{testKey: false}},
+ {method: "POST", param: map[string]interface{}{testKey: "false"}},
+ {method: "POST", param: map[string]interface{}{testKey: "0"}},
+ {method: "POST", param: map[string]interface{}{testKey: ""}},
+ } {
+ c.Logf("#%d, tr: %#v", i, tr)
+ req := tr.Request()
+ c.Logf("tr.body: %s", tr.bodyContent())
+ params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+ c.Logf("params: %#v", params)
+ c.Assert(err, check.IsNil)
+ c.Check(params, check.NotNil)
+ c.Check(params[testKey], check.Equals, false)
+ }
+
+ for i, tr := range []testReq{
+ {method: "POST", param: map[string]interface{}{testKey: true}, json: true},
+ {method: "POST", param: map[string]interface{}{testKey: true}},
+ {method: "POST", param: map[string]interface{}{testKey: "true"}},
+ {method: "POST", param: map[string]interface{}{testKey: "1"}},
+ } {
+ c.Logf("#%d, tr: %#v", i, tr)
+ req := tr.Request()
+ c.Logf("tr.body: %s", tr.bodyContent())
+ params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+ c.Logf("params: %#v", params)
+ c.Assert(err, check.IsNil)
+ c.Check(params, check.NotNil)
+ c.Check(params[testKey], check.Equals, true)
+ }
+}
+
+func (s *RouterSuite) TestOrderParam(c *check.C) {
+ for i, tr := range []testReq{
+ {method: "POST", param: map[string]interface{}{"order": ""}, json: true},
+ {method: "POST", param: map[string]interface{}{"order": ""}, json: false},
+ {method: "POST", param: map[string]interface{}{"order": []string{}}, json: true},
+ {method: "POST", param: map[string]interface{}{"order": []string{}}, json: false},
+ {method: "POST", param: map[string]interface{}{}, json: true},
+ {method: "POST", param: map[string]interface{}{}, json: false},
+ } {
+ c.Logf("#%d, tr: %#v", i, tr)
+ req := tr.Request()
+ params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+ c.Assert(err, check.IsNil)
+ c.Assert(params, check.NotNil)
+ if order, ok := params["order"]; ok && order != nil {
+ c.Check(order, check.DeepEquals, []interface{}{})
+ }
+ }
+
+ for i, tr := range []testReq{
+ {method: "POST", param: map[string]interface{}{"order": "foo,bar desc"}, json: true},
+ {method: "POST", param: map[string]interface{}{"order": "foo,bar desc"}, json: false},
+ {method: "POST", param: map[string]interface{}{"order": "[\"foo\", \"bar desc\"]"}, json: false},
+ {method: "POST", param: map[string]interface{}{"order": []string{"foo", "bar desc"}}, json: true},
+ {method: "POST", param: map[string]interface{}{"order": []string{"foo", "bar desc"}}, json: false},
+ } {
+ c.Logf("#%d, tr: %#v", i, tr)
+ req := tr.Request()
+ params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+ c.Assert(err, check.IsNil)
+ if _, ok := params["order"].([]string); ok {
+ c.Check(params["order"], check.DeepEquals, []string{"foo", "bar desc"})
+ } else {
+ c.Check(params["order"], check.DeepEquals, []interface{}{"foo", "bar desc"})
+ }
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/httpserver"
+)
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+type responseOptions struct {
+ Select []string
+ Count string
+}
+
+func (rtr *router) responseOptions(opts interface{}) (responseOptions, error) {
+ var rOpts responseOptions
+ switch opts := opts.(type) {
+ case *arvados.GetOptions:
+ rOpts.Select = opts.Select
+ case *arvados.ListOptions:
+ rOpts.Select = opts.Select
+ rOpts.Count = opts.Count
+ }
+ return rOpts, nil
+}
+
+func applySelectParam(selectParam []string, orig map[string]interface{}) map[string]interface{} {
+ if len(selectParam) == 0 {
+ return orig
+ }
+ selected := map[string]interface{}{}
+ for _, attr := range selectParam {
+ if v, ok := orig[attr]; ok {
+ selected[attr] = v
+ }
+ }
+ // Preserve "kind" even if not requested
+ if v, ok := orig["kind"]; ok {
+ selected["kind"] = v
+ }
+ return selected
+}
+
+func (rtr *router) sendResponse(w http.ResponseWriter, resp interface{}, opts responseOptions) {
+ var tmp map[string]interface{}
+
+ err := rtr.transcode(resp, &tmp)
+ if err != nil {
+ rtr.sendError(w, err)
+ return
+ }
+
+ respKind := kind(resp)
+ if respKind != "" {
+ tmp["kind"] = respKind
+ }
+ defaultItemKind := ""
+ if strings.HasSuffix(respKind, "List") {
+ defaultItemKind = strings.TrimSuffix(respKind, "List")
+ }
+
+ if items, ok := tmp["items"].([]interface{}); ok {
+ for i, item := range items {
+ // Fill in "kind" by inspecting UUID/PDH if
+ // possible; fall back on assuming each
+ // Items[] entry in an "arvados#fooList"
+ // response should have kind="arvados#foo".
+ item, _ := item.(map[string]interface{})
+ infix := ""
+ if uuid, _ := item["uuid"].(string); len(uuid) == 27 {
+ infix = uuid[6:11]
+ }
+ if k := kind(infixMap[infix]); k != "" {
+ item["kind"] = k
+ } else if pdh, _ := item["portable_data_hash"].(string); pdh != "" {
+ item["kind"] = "arvados#collection"
+ } else if defaultItemKind != "" {
+ item["kind"] = defaultItemKind
+ }
+ items[i] = applySelectParam(opts.Select, item)
+ }
+ if opts.Count == "none" {
+ delete(tmp, "items_available")
+ }
+ } else {
+ tmp = applySelectParam(opts.Select, tmp)
+ }
+
+ // Format non-nil timestamps as rfc3339NanoFixed (by default
+ // they will have been encoded to time.RFC3339Nano, which
+ // omits trailing zeroes).
+ for k, v := range tmp {
+ if !strings.HasSuffix(k, "_at") {
+ continue
+ }
+ switch tv := v.(type) {
+ case *time.Time:
+ if tv == nil {
+ break
+ }
+ tmp[k] = tv.Format(rfc3339NanoFixed)
+ case time.Time:
+ tmp[k] = tv.Format(rfc3339NanoFixed)
+ case string:
+ t, err := time.Parse(time.RFC3339Nano, tv)
+ if err != nil {
+ break
+ }
+ tmp[k] = t.Format(rfc3339NanoFixed)
+ }
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(tmp)
+}
+
+func (rtr *router) sendError(w http.ResponseWriter, err error) {
+ code := http.StatusInternalServerError
+ if err, ok := err.(interface{ HTTPStatus() int }); ok {
+ code = err.HTTPStatus()
+ }
+ httpserver.Error(w, err.Error(), code)
+}
+
+var infixMap = map[string]interface{}{
+ "4zz18": arvados.Collection{},
+ "j7d0g": arvados.Group{},
+}
+
+var mungeKind = regexp.MustCompile(`\..`)
+
+func kind(resp interface{}) string {
+ t := fmt.Sprintf("%T", resp)
+ if !strings.HasPrefix(t, "arvados.") {
+ return ""
+ }
+ return mungeKind.ReplaceAllStringFunc(t, func(s string) string {
+ // "arvados.CollectionList" => "arvados#collectionList"
+ return "#" + strings.ToLower(s[1:])
+ })
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "git.curoverse.com/arvados.git/lib/controller/federation"
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+ "git.curoverse.com/arvados.git/sdk/go/httpserver"
+ "github.com/julienschmidt/httprouter"
+ "github.com/sirupsen/logrus"
+)
+
+type router struct {
+ mux *httprouter.Router
+ fed arvados.API
+}
+
+func New(cluster *arvados.Cluster) *router {
+ rtr := &router{
+ mux: httprouter.New(),
+ fed: federation.New(cluster),
+ }
+ rtr.addRoutes()
+ return rtr
+}
+
+type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+
+func (rtr *router) addRoutes() {
+ for _, route := range []struct {
+ endpoint arvados.APIEndpoint
+ defaultOpts func() interface{}
+ exec routableFunc
+ }{
+ {
+ arvados.EndpointCollectionCreate,
+ func() interface{} { return &arvados.CreateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionUpdate,
+ func() interface{} { return &arvados.UpdateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionGet,
+ func() interface{} { return &arvados.GetOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionList,
+ func() interface{} { return &arvados.ListOptions{Limit: -1} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionProvenance,
+ func() interface{} { return &arvados.GetOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionUsedBy,
+ func() interface{} { return &arvados.GetOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionDelete,
+ func() interface{} { return &arvados.DeleteOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionTrash,
+ func() interface{} { return &arvados.DeleteOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
+ },
+ },
+ {
+ arvados.EndpointCollectionUntrash,
+ func() interface{} { return &arvados.UntrashOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerCreate,
+ func() interface{} { return &arvados.CreateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerUpdate,
+ func() interface{} { return &arvados.UpdateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerGet,
+ func() interface{} { return &arvados.GetOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerList,
+ func() interface{} { return &arvados.ListOptions{Limit: -1} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerDelete,
+ func() interface{} { return &arvados.DeleteOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerLock,
+ func() interface{} {
+ return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
+ },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointContainerUnlock,
+ func() interface{} {
+ return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
+ },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointSpecimenCreate,
+ func() interface{} { return &arvados.CreateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
+ },
+ },
+ {
+ arvados.EndpointSpecimenUpdate,
+ func() interface{} { return &arvados.UpdateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ },
+ },
+ {
+ arvados.EndpointSpecimenGet,
+ func() interface{} { return &arvados.GetOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
+ },
+ },
+ {
+ arvados.EndpointSpecimenList,
+ func() interface{} { return &arvados.ListOptions{Limit: -1} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
+ },
+ },
+ {
+ arvados.EndpointSpecimenDelete,
+ func() interface{} { return &arvados.DeleteOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
+ },
+ },
+ } {
+ rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
+ if route.endpoint.Method == "PATCH" {
+ // Accept PUT as a synonym for PATCH.
+ endpointPUT := route.endpoint
+ endpointPUT.Method = "PUT"
+ rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
+ }
+ }
+ rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
+ })
+ rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
+ })
+}
+
+func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
+ rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
+ logger := ctxlog.FromContext(req.Context())
+ params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
+ if err != nil {
+ logger.WithFields(logrus.Fields{
+ "req": req,
+ "method": endpoint.Method,
+ "endpoint": endpoint,
+ }).WithError(err).Debug("error loading request params")
+ rtr.sendError(w, err)
+ return
+ }
+ opts := defaultOpts()
+ err = rtr.transcode(params, opts)
+ if err != nil {
+ logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
+ rtr.sendError(w, err)
+ return
+ }
+ respOpts, err := rtr.responseOptions(opts)
+ if err != nil {
+ logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
+ rtr.sendError(w, err)
+ return
+ }
+
+ creds := auth.CredentialsFromRequest(req)
+ if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
+ for _, t := range rt {
+ if t, ok := t.(string); ok {
+ creds.Tokens = append(creds.Tokens, t)
+ }
+ }
+ }
+ ctx := auth.NewContext(req.Context(), creds)
+ ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
+ logger.WithFields(logrus.Fields{
+ "apiEndpoint": endpoint,
+ "apiOptsType": fmt.Sprintf("%T", opts),
+ "apiOpts": opts,
+ }).Debug("exec")
+ resp, err := exec(ctx, opts)
+ if err != nil {
+ logger.WithError(err).Debugf("returning error type %T", err)
+ rtr.sendError(w, err)
+ return
+ }
+ rtr.sendResponse(w, resp, respOpts)
+ })
+}
+
+func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
+ case "login", "logout", "auth":
+ default:
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
+ w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
+ w.Header().Set("Access-Control-Max-Age", "86486400")
+ }
+ if r.Method == "OPTIONS" {
+ return
+ }
+ r.ParseForm()
+ if m := r.FormValue("_method"); m != "" {
+ r2 := *r
+ r = &r2
+ r.Method = m
+ }
+ rtr.mux.ServeHTTP(w, r)
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+ "github.com/julienschmidt/httprouter"
+ check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+ check.TestingT(t)
+}
+
+var _ = check.Suite(&RouterSuite{})
+
+type RouterSuite struct {
+ rtr *router
+ stub arvadostest.APIStub
+}
+
+func (s *RouterSuite) SetUpTest(c *check.C) {
+ s.stub = arvadostest.APIStub{}
+ s.rtr = &router{
+ mux: httprouter.New(),
+ fed: &s.stub,
+ }
+ s.rtr.addRoutes()
+}
+
+func (s *RouterSuite) TestOptions(c *check.C) {
+ token := arvadostest.ActiveToken
+ for _, trial := range []struct {
+ method string
+ path string
+ header http.Header
+ body string
+ shouldStatus int // zero value means 200
+ shouldCall string
+ withOptions interface{}
+ }{
+ {
+ method: "GET",
+ path: "/arvados/v1/collections/" + arvadostest.FooCollection,
+ shouldCall: "CollectionGet",
+ withOptions: arvados.GetOptions{UUID: arvadostest.FooCollection},
+ },
+ {
+ method: "PUT",
+ path: "/arvados/v1/collections/" + arvadostest.FooCollection,
+ shouldCall: "CollectionUpdate",
+ withOptions: arvados.UpdateOptions{UUID: arvadostest.FooCollection},
+ },
+ {
+ method: "PATCH",
+ path: "/arvados/v1/collections/" + arvadostest.FooCollection,
+ shouldCall: "CollectionUpdate",
+ withOptions: arvados.UpdateOptions{UUID: arvadostest.FooCollection},
+ },
+ {
+ method: "DELETE",
+ path: "/arvados/v1/collections/" + arvadostest.FooCollection,
+ shouldCall: "CollectionDelete",
+ withOptions: arvados.DeleteOptions{UUID: arvadostest.FooCollection},
+ },
+ {
+ method: "POST",
+ path: "/arvados/v1/collections",
+ shouldCall: "CollectionCreate",
+ withOptions: arvados.CreateOptions{},
+ },
+ {
+ method: "GET",
+ path: "/arvados/v1/collections",
+ shouldCall: "CollectionList",
+ withOptions: arvados.ListOptions{Limit: -1},
+ },
+ {
+ method: "GET",
+ path: "/arvados/v1/collections?limit=123&offset=456&include_trash=true&include_old_versions=1",
+ shouldCall: "CollectionList",
+ withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+ },
+ {
+ method: "POST",
+ path: "/arvados/v1/collections?limit=123&_method=GET",
+ body: `{"offset":456,"include_trash":true,"include_old_versions":true}`,
+ shouldCall: "CollectionList",
+ withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+ },
+ {
+ method: "POST",
+ path: "/arvados/v1/collections?limit=123",
+ body: "offset=456&include_trash=true&include_old_versions=1&_method=GET",
+ header: http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
+ shouldCall: "CollectionList",
+ withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+ },
+ {
+ method: "PATCH",
+ path: "/arvados/v1/collections",
+ shouldStatus: http.StatusMethodNotAllowed,
+ },
+ {
+ method: "PUT",
+ path: "/arvados/v1/collections",
+ shouldStatus: http.StatusMethodNotAllowed,
+ },
+ {
+ method: "DELETE",
+ path: "/arvados/v1/collections",
+ shouldStatus: http.StatusMethodNotAllowed,
+ },
+ } {
+ // Reset calls captured in previous trial
+ s.stub = arvadostest.APIStub{}
+
+ c.Logf("trial: %#v", trial)
+ _, rr, _ := doRequest(c, s.rtr, token, trial.method, trial.path, trial.header, bytes.NewBufferString(trial.body))
+ if trial.shouldStatus == 0 {
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ } else {
+ c.Check(rr.Code, check.Equals, trial.shouldStatus)
+ }
+ calls := s.stub.Calls(nil)
+ if trial.shouldCall == "" {
+ c.Check(calls, check.HasLen, 0)
+ } else if len(calls) != 1 {
+ c.Check(calls, check.HasLen, 1)
+ } else {
+ c.Check(calls[0].Method, isMethodNamed, trial.shouldCall)
+ c.Check(calls[0].Options, check.DeepEquals, trial.withOptions)
+ }
+ }
+}
+
+var _ = check.Suite(&RouterIntegrationSuite{})
+
+type RouterIntegrationSuite struct {
+ rtr *router
+}
+
+func (s *RouterIntegrationSuite) SetUpTest(c *check.C) {
+ cluster := &arvados.Cluster{}
+ cluster.TLS.Insecure = true
+ arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+ s.rtr = New(cluster)
+}
+
+func (s *RouterIntegrationSuite) TearDownSuite(c *check.C) {
+ err := arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil)
+ c.Check(err, check.IsNil)
+}
+
+func (s *RouterIntegrationSuite) TestCollectionResponses(c *check.C) {
+ token := arvadostest.ActiveTokenV2
+
+ // Check "get collection" response has "kind" key
+ _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections`, nil, bytes.NewBufferString(`{"include_trash":true}`))
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["items"], check.FitsTypeOf, []interface{}{})
+ c.Check(jresp["kind"], check.Equals, "arvados#collectionList")
+ c.Check(jresp["items"].([]interface{})[0].(map[string]interface{})["kind"], check.Equals, "arvados#collection")
+
+ // Check items in list response have a "kind" key regardless
+ // of whether a uuid/pdh is selected.
+ for _, selectj := range []string{
+ ``,
+ `,"select":["portable_data_hash"]`,
+ `,"select":["name"]`,
+ `,"select":["uuid"]`,
+ } {
+ _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections`, nil, bytes.NewBufferString(`{"where":{"uuid":["`+arvadostest.FooCollection+`"]}`+selectj+`}`))
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["items"], check.FitsTypeOf, []interface{}{})
+ c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+ c.Check(jresp["kind"], check.Equals, "arvados#collectionList")
+ item0 := jresp["items"].([]interface{})[0].(map[string]interface{})
+ c.Check(item0["kind"], check.Equals, "arvados#collection")
+ if selectj == "" || strings.Contains(selectj, "portable_data_hash") {
+ c.Check(item0["portable_data_hash"], check.Equals, arvadostest.FooCollectionPDH)
+ } else {
+ c.Check(item0["portable_data_hash"], check.IsNil)
+ }
+ if selectj == "" || strings.Contains(selectj, "name") {
+ c.Check(item0["name"], check.FitsTypeOf, "")
+ } else {
+ c.Check(item0["name"], check.IsNil)
+ }
+ if selectj == "" || strings.Contains(selectj, "uuid") {
+ c.Check(item0["uuid"], check.Equals, arvadostest.FooCollection)
+ } else {
+ c.Check(item0["uuid"], check.IsNil)
+ }
+ }
+
+ // Check "create collection" response has "kind" key
+ _, rr, jresp = doRequest(c, s.rtr, token, "POST", `/arvados/v1/collections`, http.Header{"Content-Type": {"application/x-www-form-urlencoded"}}, bytes.NewBufferString(`ensure_unique_name=true`))
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["uuid"], check.FitsTypeOf, "")
+ c.Check(jresp["kind"], check.Equals, "arvados#collection")
+}
+
+func (s *RouterIntegrationSuite) TestContainerList(c *check.C) {
+ token := arvadostest.ActiveTokenV2
+
+ _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?limit=0`, nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+ c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+ c.Check(jresp["items"], check.HasLen, 0)
+
+ _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?limit=2&select=["uuid","command"]`, nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+ c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+ c.Check(jresp["items"], check.HasLen, 2)
+ item0 := jresp["items"].([]interface{})[0].(map[string]interface{})
+ c.Check(item0["uuid"], check.HasLen, 27)
+ c.Check(item0["command"], check.FitsTypeOf, []interface{}{})
+ c.Check(item0["command"].([]interface{})[0], check.FitsTypeOf, "")
+ c.Check(item0["mounts"], check.IsNil)
+
+ _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers`, nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+ c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+ avail := int(jresp["items_available"].(float64))
+ c.Check(jresp["items"], check.HasLen, avail)
+ item0 = jresp["items"].([]interface{})[0].(map[string]interface{})
+ c.Check(item0["uuid"], check.HasLen, 27)
+ c.Check(item0["command"], check.FitsTypeOf, []interface{}{})
+ c.Check(item0["command"].([]interface{})[0], check.FitsTypeOf, "")
+ c.Check(item0["mounts"], check.NotNil)
+}
+
+func (s *RouterIntegrationSuite) TestContainerLock(c *check.C) {
+ uuid := arvadostest.QueuedContainerUUID
+ token := arvadostest.AdminToken
+ _, rr, jresp := doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/lock", nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["uuid"], check.HasLen, 27)
+ c.Check(jresp["state"], check.Equals, "Locked")
+ _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/lock", nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusUnprocessableEntity)
+ c.Check(rr.Body.String(), check.Not(check.Matches), `.*"uuid":.*`)
+ _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/unlock", nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["uuid"], check.HasLen, 27)
+ c.Check(jresp["state"], check.Equals, "Queued")
+ c.Check(jresp["environment"], check.IsNil)
+ _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/unlock", nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusUnprocessableEntity)
+ c.Check(jresp["uuid"], check.IsNil)
+}
+
+func (s *RouterIntegrationSuite) TestFullTimestampsInResponse(c *check.C) {
+ uuid := arvadostest.CollectionReplicationDesired2Confirmed2UUID
+ token := arvadostest.ActiveTokenV2
+
+ _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections/`+uuid, nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(jresp["uuid"], check.Equals, uuid)
+ expectNS := map[string]int{
+ "created_at": 596506000, // fixture says 596506247, but truncated by postgresql
+ "modified_at": 596338000, // fixture says 596338465, but truncated by postgresql
+ }
+ for key, ns := range expectNS {
+ mt, ok := jresp[key].(string)
+ c.Logf("jresp[%q] == %q", key, mt)
+ c.Assert(ok, check.Equals, true)
+ t, err := time.Parse(time.RFC3339Nano, mt)
+ c.Check(err, check.IsNil)
+ c.Check(t.Nanosecond(), check.Equals, ns)
+ }
+}
+
+func (s *RouterIntegrationSuite) TestSelectParam(c *check.C) {
+ uuid := arvadostest.QueuedContainerUUID
+ token := arvadostest.ActiveTokenV2
+ for _, sel := range [][]string{
+ {"uuid", "command"},
+ {"uuid", "command", "uuid"},
+ {"", "command", "uuid"},
+ } {
+ j, err := json.Marshal(sel)
+ c.Assert(err, check.IsNil)
+ _, rr, resp := doRequest(c, s.rtr, token, "GET", "/arvados/v1/containers/"+uuid+"?select="+string(j), nil, nil)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+
+ c.Check(resp["kind"], check.Equals, "arvados#container")
+ c.Check(resp["uuid"], check.HasLen, 27)
+ c.Check(resp["command"], check.HasLen, 2)
+ c.Check(resp["mounts"], check.IsNil)
+ _, hasMounts := resp["mounts"]
+ c.Check(hasMounts, check.Equals, false)
+ }
+}
+
+func (s *RouterIntegrationSuite) TestRouteNotFound(c *check.C) {
+ token := arvadostest.ActiveTokenV2
+ req := (&testReq{
+ method: "POST",
+ path: "arvados/v1/collections/" + arvadostest.FooCollection + "/error404pls",
+ token: token,
+ }).Request()
+ rr := httptest.NewRecorder()
+ s.rtr.ServeHTTP(rr, req)
+ c.Check(rr.Code, check.Equals, http.StatusNotFound)
+ c.Logf("body: %q", rr.Body.String())
+ var j map[string]interface{}
+ err := json.Unmarshal(rr.Body.Bytes(), &j)
+ c.Check(err, check.IsNil)
+ c.Logf("decoded: %v", j)
+ c.Assert(j["errors"], check.FitsTypeOf, []interface{}{})
+ c.Check(j["errors"].([]interface{})[0], check.Equals, "API endpoint not found")
+}
+
+func (s *RouterIntegrationSuite) TestCORS(c *check.C) {
+ token := arvadostest.ActiveTokenV2
+ req := (&testReq{
+ method: "OPTIONS",
+ path: "arvados/v1/collections/" + arvadostest.FooCollection,
+ header: http.Header{"Origin": {"https://example.com"}},
+ token: token,
+ }).Request()
+ rr := httptest.NewRecorder()
+ s.rtr.ServeHTTP(rr, req)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(rr.Body.String(), check.HasLen, 0)
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "*")
+ for _, hdr := range []string{"Authorization", "Content-Type"} {
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Matches, ".*"+hdr+".*")
+ }
+ for _, method := range []string{"GET", "HEAD", "PUT", "POST", "DELETE"} {
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Matches, ".*"+method+".*")
+ }
+
+ for _, unsafe := range []string{"login", "logout", "auth", "auth/foo", "login/?blah"} {
+ req := (&testReq{
+ method: "OPTIONS",
+ path: unsafe,
+ header: http.Header{"Origin": {"https://example.com"}},
+ token: token,
+ }).Request()
+ rr := httptest.NewRecorder()
+ s.rtr.ServeHTTP(rr, req)
+ c.Check(rr.Code, check.Equals, http.StatusOK)
+ c.Check(rr.Body.String(), check.HasLen, 0)
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "")
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Equals, "")
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Equals, "")
+
+ req = (&testReq{
+ method: "POST",
+ path: unsafe,
+ header: http.Header{"Origin": {"https://example.com"}},
+ token: token,
+ }).Request()
+ rr = httptest.NewRecorder()
+ s.rtr.ServeHTTP(rr, req)
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "")
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Equals, "")
+ c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Equals, "")
+ }
+}
+
+func doRequest(c *check.C, rtr http.Handler, token, method, path string, hdrs http.Header, body io.Reader) (*http.Request, *httptest.ResponseRecorder, map[string]interface{}) {
+ req := httptest.NewRequest(method, path, body)
+ for k, v := range hdrs {
+ req.Header[k] = v
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+ rr := httptest.NewRecorder()
+ rtr.ServeHTTP(rr, req)
+ c.Logf("response body: %s", rr.Body.String())
+ var jresp map[string]interface{}
+ err := json.Unmarshal(rr.Body.Bytes(), &jresp)
+ c.Check(err, check.IsNil)
+ return req, rr, jresp
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package rpc
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+type TokenProvider func(context.Context) ([]string, error)
+
+type Conn struct {
+ clusterID string
+ httpClient http.Client
+ baseURL url.URL
+ tokenProvider TokenProvider
+}
+
+func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *Conn {
+ transport := http.DefaultTransport
+ if insecure {
+ // It's not safe to copy *http.DefaultTransport
+ // because it has a mutex (which might be locked)
+ // protecting a private map (which might not be nil).
+ // So we build our own, using the Go 1.12 default
+ // values, ignoring any changes the application has
+ // made to http.DefaultTransport.
+ transport = &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ DualStack: true,
+ }).DialContext,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ }
+ return &Conn{
+ clusterID: clusterID,
+ httpClient: http.Client{Transport: transport},
+ baseURL: *url,
+ tokenProvider: tp,
+ }
+}
+
+func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
+ aClient := arvados.Client{
+ Client: &conn.httpClient,
+ Scheme: conn.baseURL.Scheme,
+ APIHost: conn.baseURL.Host,
+ }
+ tokens, err := conn.tokenProvider(ctx)
+ if err != nil {
+ return err
+ } else if len(tokens) > 0 {
+ ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+tokens[0])
+ } else {
+ // Use a non-empty auth string to ensure we override
+ // any default token set on aClient -- and to avoid
+ // having the remote prompt us to send a token by
+ // responding 401.
+ ctx = arvados.ContextWithAuthorization(ctx, "Bearer -")
+ }
+
+ // Encode opts to JSON and decode from there to a
+ // map[string]interface{}, so we can munge the query params
+ // using the JSON key names specified by opts' struct tags.
+ j, err := json.Marshal(opts)
+ if err != nil {
+ return fmt.Errorf("%T: requestAndDecode: Marshal opts: %s", conn, err)
+ }
+ var params map[string]interface{}
+ err = json.Unmarshal(j, ¶ms)
+ if err != nil {
+ return fmt.Errorf("%T: requestAndDecode: Unmarshal opts: %s", conn, err)
+ }
+ if attrs, ok := params["attrs"]; ok && ep.AttrsKey != "" {
+ params[ep.AttrsKey] = attrs
+ delete(params, "attrs")
+ }
+ if limit, ok := params["limit"].(float64); ok && limit < 0 {
+ // Negative limit means "not specified" here, but some
+ // servers/versions do not accept that, so we need to
+ // remove it entirely.
+ delete(params, "limit")
+ }
+ if len(tokens) > 1 {
+ params["reader_tokens"] = tokens[1:]
+ }
+ path := ep.Path
+ if strings.Contains(ep.Path, "/:uuid") {
+ uuid, _ := params["uuid"].(string)
+ path = strings.Replace(path, "/:uuid", "/"+uuid, 1)
+ delete(params, "uuid")
+ }
+ return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
+}
+
+func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionCreate
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionUpdate
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionGet
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+ ep := arvados.EndpointCollectionList
+ var resp arvados.CollectionList
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ ep := arvados.EndpointCollectionProvenance
+ var resp map[string]interface{}
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ ep := arvados.EndpointCollectionUsedBy
+ var resp map[string]interface{}
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionDelete
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionTrash
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+ ep := arvados.EndpointCollectionUntrash
+ var resp arvados.Collection
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerCreate
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerUpdate
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerGet
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+ ep := arvados.EndpointContainerList
+ var resp arvados.ContainerList
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerDelete
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerLock
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ ep := arvados.EndpointContainerUnlock
+ var resp arvados.Container
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+ ep := arvados.EndpointSpecimenCreate
+ var resp arvados.Specimen
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+ ep := arvados.EndpointSpecimenUpdate
+ var resp arvados.Specimen
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+ ep := arvados.EndpointSpecimenGet
+ var resp arvados.Specimen
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+ ep := arvados.EndpointSpecimenList
+ var resp arvados.SpecimenList
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+ ep := arvados.EndpointSpecimenDelete
+ var resp arvados.Specimen
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
+func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+ ep := arvados.EndpointAPIClientAuthorizationCurrent
+ var resp arvados.APIClientAuthorization
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package rpc
+
+import (
+ "context"
+ "net/url"
+ "os"
+ "testing"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+ "github.com/sirupsen/logrus"
+ check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+ check.TestingT(t)
+}
+
+var _ = check.Suite(&RPCSuite{})
+
+const contextKeyTestTokens = "testTokens"
+
+type RPCSuite struct {
+ log logrus.FieldLogger
+ ctx context.Context
+ conn *Conn
+}
+
+func (s *RPCSuite) SetUpTest(c *check.C) {
+ ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
+ s.ctx = context.WithValue(ctx, contextKeyTestTokens, []string{arvadostest.ActiveToken})
+ s.conn = NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_TEST_API_HOST")}, true, func(ctx context.Context) ([]string, error) {
+ return ctx.Value(contextKeyTestTokens).([]string), nil
+ })
+}
+
+func (s *RPCSuite) TestCollectionCreate(c *check.C) {
+ coll, err := s.conn.CollectionCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
+ "owner_uuid": arvadostest.ActiveUserUUID,
+ "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0",
+ }})
+ c.Check(err, check.IsNil)
+ c.Check(coll.UUID, check.HasLen, 27)
+}
+
+func (s *RPCSuite) TestSpecimenCRUD(c *check.C) {
+ sp, err := s.conn.SpecimenCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
+ "owner_uuid": arvadostest.ActiveUserUUID,
+ "properties": map[string]string{"foo": "bar"},
+ }})
+ c.Check(err, check.IsNil)
+ c.Check(sp.UUID, check.HasLen, 27)
+ c.Check(sp.Properties, check.HasLen, 1)
+ c.Check(sp.Properties["foo"], check.Equals, "bar")
+
+ spGet, err := s.conn.SpecimenGet(s.ctx, arvados.GetOptions{UUID: sp.UUID})
+ c.Check(spGet.UUID, check.Equals, sp.UUID)
+ c.Check(spGet.Properties["foo"], check.Equals, "bar")
+
+ spList, err := s.conn.SpecimenList(s.ctx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", sp.UUID}}})
+ c.Check(spList.ItemsAvailable, check.Equals, 1)
+ c.Assert(spList.Items, check.HasLen, 1)
+ c.Check(spList.Items[0].UUID, check.Equals, sp.UUID)
+ c.Check(spList.Items[0].Properties["foo"], check.Equals, "bar")
+
+ anonCtx := context.WithValue(context.Background(), contextKeyTestTokens, []string{arvadostest.AnonymousToken})
+ spList, err = s.conn.SpecimenList(anonCtx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", sp.UUID}}})
+ c.Check(spList.ItemsAvailable, check.Equals, 0)
+ c.Check(spList.Items, check.HasLen, 0)
+
+ spDel, err := s.conn.SpecimenDelete(s.ctx, arvados.DeleteOptions{UUID: sp.UUID})
+ c.Check(spDel.UUID, check.Equals, sp.UUID)
+}
package controller
import (
+ "context"
"net/http"
"os"
"path/filepath"
handler := &Handler{Cluster: &arvados.Cluster{
ClusterID: "zzzzz",
PostgreSQL: integrationTestCluster().PostgreSQL,
+
+ EnableBetaController14287: enableBetaController14287,
}}
handler.Cluster.TLS.Insecure = true
arvadostest.SetServiceURL(&handler.Cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
srv := &httpserver.Server{
Server: http.Server{
- Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
+ Handler: httpserver.HandlerWithContext(
+ ctxlog.Context(context.Background(), log),
+ httpserver.AddRequestIDs(httpserver.LogRequests(handler))),
},
Addr: ":",
}
}
srv := &httpserver.Server{
Server: http.Server{
- Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
+ Handler: httpserver.HandlerWithContext(ctx,
+ httpserver.AddRequestIDs(httpserver.LogRequests(handler))),
},
Addr: listen,
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "context"
+
+type APIEndpoint struct {
+ Method string
+ Path string
+ // "new attributes" key for create/update requests
+ AttrsKey string
+}
+
+var (
+ EndpointCollectionCreate = APIEndpoint{"POST", "arvados/v1/collections", "collection"}
+ EndpointCollectionUpdate = APIEndpoint{"PATCH", "arvados/v1/collections/:uuid", "collection"}
+ EndpointCollectionGet = APIEndpoint{"GET", "arvados/v1/collections/:uuid", ""}
+ EndpointCollectionList = APIEndpoint{"GET", "arvados/v1/collections", ""}
+ EndpointCollectionProvenance = APIEndpoint{"GET", "arvados/v1/collections/:uuid/provenance", ""}
+ EndpointCollectionUsedBy = APIEndpoint{"GET", "arvados/v1/collections/:uuid/used_by", ""}
+ EndpointCollectionDelete = APIEndpoint{"DELETE", "arvados/v1/collections/:uuid", ""}
+ EndpointCollectionTrash = APIEndpoint{"POST", "arvados/v1/collections/:uuid/trash", ""}
+ EndpointCollectionUntrash = APIEndpoint{"POST", "arvados/v1/collections/:uuid/untrash", ""}
+ EndpointSpecimenCreate = APIEndpoint{"POST", "arvados/v1/specimens", "specimen"}
+ EndpointSpecimenUpdate = APIEndpoint{"PATCH", "arvados/v1/specimens/:uuid", "specimen"}
+ EndpointSpecimenGet = APIEndpoint{"GET", "arvados/v1/specimens/:uuid", ""}
+ EndpointSpecimenList = APIEndpoint{"GET", "arvados/v1/specimens", ""}
+ EndpointSpecimenDelete = APIEndpoint{"DELETE", "arvados/v1/specimens/:uuid", ""}
+ EndpointContainerCreate = APIEndpoint{"POST", "arvados/v1/containers", "container"}
+ EndpointContainerUpdate = APIEndpoint{"PATCH", "arvados/v1/containers/:uuid", "container"}
+ EndpointContainerGet = APIEndpoint{"GET", "arvados/v1/containers/:uuid", ""}
+ EndpointContainerList = APIEndpoint{"GET", "arvados/v1/containers", ""}
+ EndpointContainerDelete = APIEndpoint{"DELETE", "arvados/v1/containers/:uuid", ""}
+ EndpointContainerLock = APIEndpoint{"POST", "arvados/v1/containers/:uuid/lock", ""}
+ EndpointContainerUnlock = APIEndpoint{"POST", "arvados/v1/containers/:uuid/unlock", ""}
+ EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
+)
+
+type GetOptions struct {
+ UUID string `json:"uuid"`
+ Select []string `json:"select"`
+ IncludeTrash bool `json:"include_trash"`
+}
+
+type UntrashOptions struct {
+ UUID string `json:"uuid"`
+ EnsureUniqueName bool `json:"ensure_unique_name"`
+}
+
+type ListOptions struct {
+ ClusterID string `json:"cluster_id"`
+ Select []string `json:"select"`
+ Filters []Filter `json:"filters"`
+ Where map[string]interface{} `json:"where"`
+ Limit int `json:"limit"`
+ Offset int `json:"offset"`
+ Order []string `json:"order"`
+ Distinct bool `json:"distinct"`
+ Count string `json:"count"`
+ IncludeTrash bool `json:"include_trash"`
+ IncludeOldVersions bool `json:"include_old_versions"`
+}
+
+type CreateOptions struct {
+ ClusterID string `json:"cluster_id"`
+ EnsureUniqueName bool `json:"ensure_unique_name"`
+ Select []string `json:"select"`
+ Attrs map[string]interface{} `json:"attrs"`
+}
+
+type UpdateOptions struct {
+ UUID string `json:"uuid"`
+ Attrs map[string]interface{} `json:"attrs"`
+}
+
+type DeleteOptions struct {
+ UUID string `json:"uuid"`
+}
+
+type API interface {
+ CollectionCreate(ctx context.Context, options CreateOptions) (Collection, error)
+ CollectionUpdate(ctx context.Context, options UpdateOptions) (Collection, error)
+ CollectionGet(ctx context.Context, options GetOptions) (Collection, error)
+ CollectionList(ctx context.Context, options ListOptions) (CollectionList, error)
+ CollectionProvenance(ctx context.Context, options GetOptions) (map[string]interface{}, error)
+ CollectionUsedBy(ctx context.Context, options GetOptions) (map[string]interface{}, error)
+ CollectionDelete(ctx context.Context, options DeleteOptions) (Collection, error)
+ CollectionTrash(ctx context.Context, options DeleteOptions) (Collection, error)
+ CollectionUntrash(ctx context.Context, options UntrashOptions) (Collection, error)
+ ContainerCreate(ctx context.Context, options CreateOptions) (Container, error)
+ ContainerUpdate(ctx context.Context, options UpdateOptions) (Container, error)
+ ContainerGet(ctx context.Context, options GetOptions) (Container, error)
+ ContainerList(ctx context.Context, options ListOptions) (ContainerList, error)
+ ContainerDelete(ctx context.Context, options DeleteOptions) (Container, error)
+ ContainerLock(ctx context.Context, options GetOptions) (Container, error)
+ ContainerUnlock(ctx context.Context, options GetOptions) (Container, error)
+ SpecimenCreate(ctx context.Context, options CreateOptions) (Specimen, error)
+ SpecimenUpdate(ctx context.Context, options UpdateOptions) (Specimen, error)
+ SpecimenGet(ctx context.Context, options GetOptions) (Specimen, error)
+ SpecimenList(ctx context.Context, options ListOptions) (SpecimenList, error)
+ SpecimenDelete(ctx context.Context, options DeleteOptions) (Specimen, error)
+ APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
+}
// APIClientAuthorization is an arvados#apiClientAuthorization resource.
type APIClientAuthorization struct {
- UUID string `json:"uuid,omitempty"`
- APIToken string `json:"api_token,omitempty"`
- ExpiresAt string `json:"expires_at,omitempty"`
- Scopes []string `json:"scopes,omitempty"`
+ UUID string `json:"uuid"`
+ APIToken string `json:"api_token"`
+ ExpiresAt string `json:"expires_at"`
+ Scopes []string `json:"scopes"`
}
// APIClientAuthorizationList is an arvados#apiClientAuthorizationList resource.
"io"
"io/ioutil"
"log"
- "math"
"net/http"
"net/url"
"os"
// DefaultSecureClient or InsecureHTTPClient will be used.
Client *http.Client `json:"-"`
+ // Protocol scheme: "http", "https", or "" (https)
+ Scheme string
+
// Hostname (or host:port) of Arvados API server.
APIHost string
return nil, fmt.Errorf("no host in config Services.Controller.ExternalURL: %v", ctrlURL)
}
return &Client{
+ Scheme: ctrlURL.Scheme,
APIHost: ctrlURL.Host,
Insecure: cluster.TLS.Insecure,
}, nil
insecure = true
}
return &Client{
+ Scheme: "https",
APIHost: os.Getenv("ARVADOS_API_HOST"),
AuthToken: os.Getenv("ARVADOS_API_TOKEN"),
Insecure: insecure,
// Do adds Authorization and X-Request-Id headers and then calls
// (*http.Client)Do().
func (c *Client) Do(req *http.Request) (*http.Response, error) {
- if c.AuthToken != "" {
+ if auth, _ := req.Context().Value(contextKeyAuthorization{}).(string); auth != "" {
+ req.Header.Add("Authorization", auth)
+ } else if c.AuthToken != "" {
req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
}
if req.Header.Get("X-Request-Id") == "" {
- reqid, _ := c.context().Value(contextKeyRequestID).(string)
+ reqid, _ := req.Context().Value(contextKeyRequestID{}).(string)
+ if reqid == "" {
+ reqid, _ = c.context().Value(contextKeyRequestID{}).(string)
+ }
if reqid == "" {
reqid = reqIDGen.Next()
}
return nil, err
}
var generic map[string]interface{}
- err = json.Unmarshal(j, &generic)
+ dec := json.NewDecoder(bytes.NewBuffer(j))
+ dec.UseNumber()
+ err = dec.Decode(&generic)
if err != nil {
return nil, err
}
urlValues.Set(k, v)
continue
}
- if v, ok := v.(float64); ok {
- // Unmarshal decodes all numbers as float64,
- // which can be written as 1.2345e4 in JSON,
- // but this form is not accepted for ints in
- // url params. If a number fits in an int64,
- // encode it as int64 rather than float64.
- if v, frac := math.Modf(v); frac == 0 && v <= math.MaxInt64 && v >= math.MinInt64 {
- urlValues.Set(k, fmt.Sprintf("%d", int64(v)))
- continue
+ if v, ok := v.(json.Number); ok {
+ urlValues.Set(k, v.String())
+ continue
+ }
+ if v, ok := v.(bool); ok {
+ if v {
+ urlValues.Set(k, "true")
+ } else {
+ // "foo=false", "foo=0", and "foo="
+ // are all taken as true strings, so
+ // don't send false values at all --
+ // rely on the default being false.
}
+ continue
}
j, err := json.Marshal(v)
if err != nil {
return nil, err
}
+ if bytes.Equal(j, []byte("null")) {
+ // don't add it to urlValues at all
+ continue
+ }
urlValues.Set(k, string(j))
}
return urlValues, nil
//
// path must not contain a query string.
func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
+ return c.RequestAndDecodeContext(c.context(), dst, method, path, body, params)
+}
+
+func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, method, path string, body io.Reader, params interface{}) error {
if body, ok := body.(io.Closer); ok {
// Ensure body is closed even if we error out early
defer body.Close()
if err != nil {
return err
}
+ req = req.WithContext(ctx)
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
return c.DoAndDecode(dst, req)
}
return bytes.NewBufferString(v.Encode())
}
-type contextKey string
-
-var contextKeyRequestID contextKey = "X-Request-Id"
-
+// WithRequestID returns a new shallow copy of c that sends the given
+// X-Request-Id value (instead of a new randomly generated one) with
+// each subsequent request that doesn't provide its own via context or
+// header.
func (c *Client) WithRequestID(reqid string) *Client {
cc := *c
- cc.ctx = context.WithValue(cc.context(), contextKeyRequestID, reqid)
+ cc.ctx = ContextWithRequestID(cc.context(), reqid)
return &cc
}
}
func (c *Client) apiURL(path string) string {
- return "https://" + c.APIHost + "/" + path
+ scheme := c.Scheme
+ if scheme == "" {
+ scheme = "https"
+ }
+ return scheme + "://" + c.APIHost + "/" + path
}
// DiscoveryDocument is the Arvados server's description of itself.
// Collection is an arvados#collection resource.
type Collection struct {
- UUID string `json:"uuid,omitempty"`
- OwnerUUID string `json:"owner_uuid,omitempty"`
- TrashAt *time.Time `json:"trash_at,omitempty"`
- ManifestText string `json:"manifest_text"`
- UnsignedManifestText string `json:"unsigned_manifest_text,omitempty"`
- Name string `json:"name,omitempty"`
- CreatedAt *time.Time `json:"created_at,omitempty"`
- ModifiedAt *time.Time `json:"modified_at,omitempty"`
- PortableDataHash string `json:"portable_data_hash,omitempty"`
- ReplicationConfirmed *int `json:"replication_confirmed,omitempty"`
- ReplicationConfirmedAt *time.Time `json:"replication_confirmed_at,omitempty"`
- ReplicationDesired *int `json:"replication_desired,omitempty"`
- StorageClassesDesired []string `json:"storage_classes_desired,omitempty"`
- StorageClassesConfirmed []string `json:"storage_classes_confirmed,omitempty"`
- StorageClassesConfirmedAt *time.Time `json:"storage_classes_confirmed_at,omitempty"`
- DeleteAt *time.Time `json:"delete_at,omitempty"`
- IsTrashed bool `json:"is_trashed,omitempty"`
+ UUID string `json:"uuid"`
+ Etag string `json:"etag"`
+ OwnerUUID string `json:"owner_uuid"`
+ TrashAt *time.Time `json:"trash_at"`
+ ManifestText string `json:"manifest_text"`
+ UnsignedManifestText string `json:"unsigned_manifest_text"`
+ Name string `json:"name"`
+ CreatedAt *time.Time `json:"created_at"`
+ ModifiedAt *time.Time `json:"modified_at"`
+ PortableDataHash string `json:"portable_data_hash"`
+ ReplicationConfirmed *int `json:"replication_confirmed"`
+ ReplicationConfirmedAt *time.Time `json:"replication_confirmed_at"`
+ ReplicationDesired *int `json:"replication_desired"`
+ StorageClassesDesired []string `json:"storage_classes_desired"`
+ StorageClassesConfirmed []string `json:"storage_classes_confirmed"`
+ StorageClassesConfirmedAt *time.Time `json:"storage_classes_confirmed_at"`
+ DeleteAt *time.Time `json:"delete_at"`
+ IsTrashed bool `json:"is_trashed"`
+ Properties map[string]interface{} `json:"properties"`
}
func (c Collection) resourceName() string {
return sds, scanner.Err()
}
-// CollectionList is an arvados#collectionList resource.
type CollectionList struct {
Items []Collection `json:"items"`
ItemsAvailable int `json:"items_available"`
UserProfileFormMessage string
VocabularyURL string
}
+
+ EnableBetaController14287 bool
}
type Services struct {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+ "context"
+)
+
+type contextKeyRequestID struct{}
+type contextKeyAuthorization struct{}
+
+func ContextWithRequestID(ctx context.Context, reqid string) context.Context {
+ return context.WithValue(ctx, contextKeyRequestID{}, reqid)
+}
+
+// ContextWithAuthorization returns a child context that (when used
+// with (*Client)RequestAndDecodeContext) sends the given
+// Authorization header value instead of the Client's default
+// AuthToken.
+func ContextWithAuthorization(ctx context.Context, value string) context.Context {
+ return context.WithValue(ctx, contextKeyAuthorization{}, value)
+}
return
}
+func (e TransactionError) HTTPStatus() int {
+ return e.StatusCode
+}
+
func newTransactionError(req *http.Request, resp *http.Response, buf []byte) *TransactionError {
var e TransactionError
if json.Unmarshal(buf, &e) != nil {
type apiClient interface {
RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error
- UpdateBody(rsc resource) io.Reader
}
UUID: fs.uuid,
ManifestText: txt,
}
- err = fs.RequestAndDecode(nil, "PUT", "arvados/v1/collections/"+fs.uuid, fs.UpdateBody(coll), map[string]interface{}{"select": []string{"uuid"}})
+ err = fs.RequestAndDecode(nil, "PUT", "arvados/v1/collections/"+fs.uuid, nil, map[string]interface{}{
+ "collection": map[string]string{
+ "manifest_text": coll.ManifestText,
+ },
+ "select": []string{"uuid"},
+ })
if err != nil {
return fmt.Errorf("sync failed: update %s: %s", fs.uuid, err)
}
}
func (s *SiteFSSuite) TestSlashInName(c *check.C) {
- badCollection := Collection{
- Name: "bad/collection",
- OwnerUUID: fixtureAProjectUUID,
- }
- err := s.client.RequestAndDecode(&badCollection, "POST", "arvados/v1/collections", s.client.UpdateBody(&badCollection), nil)
+ var badCollection Collection
+ err := s.client.RequestAndDecode(&badCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+ "collection": map[string]string{
+ "name": "bad/collection",
+ "owner_uuid": fixtureAProjectUUID,
+ },
+ })
c.Assert(err, check.IsNil)
defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+badCollection.UUID, nil, nil)
- badProject := Group{
- Name: "bad/project",
- GroupClass: "project",
- OwnerUUID: fixtureAProjectUUID,
- }
- err = s.client.RequestAndDecode(&badProject, "POST", "arvados/v1/groups", s.client.UpdateBody(&badProject), nil)
+ var badProject Group
+ err = s.client.RequestAndDecode(&badProject, "POST", "arvados/v1/groups", nil, map[string]interface{}{
+ "group": map[string]string{
+ "name": "bad/project",
+ "group_class": "project",
+ "owner_uuid": fixtureAProjectUUID,
+ },
+ })
c.Assert(err, check.IsNil)
defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/groups/"+badProject.UUID, nil, nil)
_, err = s.fs.Open("/home/A Project/oob")
c.Check(err, check.NotNil)
- oob := Collection{
- Name: "oob",
- OwnerUUID: fixtureAProjectUUID,
- }
- err = s.client.RequestAndDecode(&oob, "POST", "arvados/v1/collections", s.client.UpdateBody(&oob), nil)
+ var oob Collection
+ err = s.client.RequestAndDecode(&oob, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+ "collection": map[string]string{
+ "name": "oob",
+ "owner_uuid": fixtureAProjectUUID,
+ },
+ })
c.Assert(err, check.IsNil)
defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+oob.UUID, nil, nil)
c.Check(err, check.IsNil)
// Delete test.txt behind s.fs's back by updating the
- // collection record with the old (empty) ManifestText.
- err = s.client.RequestAndDecode(nil, "PATCH", "arvados/v1/collections/"+oob.UUID, s.client.UpdateBody(&oob), nil)
+ // collection record with an empty ManifestText.
+ err = s.client.RequestAndDecode(nil, "PATCH", "arvados/v1/collections/"+oob.UUID, nil, map[string]interface{}{
+ "collection": map[string]string{
+ "manifest_text": "",
+ "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0",
+ },
+ })
c.Assert(err, check.IsNil)
err = project.Sync()
// Group is an arvados#group record
type Group struct {
- UUID string `json:"uuid,omitempty"`
- Name string `json:"name,omitempty"`
- OwnerUUID string `json:"owner_uuid,omitempty"`
+ UUID string `json:"uuid"`
+ Name string `json:"name"`
+ OwnerUUID string `json:"owner_uuid"`
GroupClass string `json:"group_class"`
}
// Link is an arvados#link record
type Link struct {
UUID string `json:"uuid,omiempty"`
- OwnerUUID string `json:"owner_uuid,omitempty"`
- Name string `json:"name,omitempty"`
- LinkClass string `json:"link_class,omitempty"`
- HeadUUID string `json:"head_uuid,omitempty"`
- HeadKind string `json:"head_kind,omitempty"`
- TailUUID string `json:"tail_uuid,omitempty"`
- TailKind string `json:"tail_kind,omitempty"`
+ OwnerUUID string `json:"owner_uuid"`
+ Name string `json:"name"`
+ LinkClass string `json:"link_class"`
+ HeadUUID string `json:"head_uuid"`
+ HeadKind string `json:"head_kind"`
+ TailUUID string `json:"tail_uuid"`
+ TailKind string `json:"tail_kind"`
}
// UserList is an arvados#userList resource.
// Log is an arvados#log record
type Log struct {
- ID uint64 `json:"id,omitempty"`
- UUID string `json:"uuid,omitempty"`
- ObjectUUID string `json:"object_uuid,omitempty"`
- ObjectOwnerUUID string `json:"object_owner_uuid,omitempty"`
- EventType string `json:"event_type,omitempty"`
- EventAt *time.Time `json:"event,omitempty"`
- Properties map[string]interface{} `json:"properties,omitempty"`
- CreatedAt *time.Time `json:"created_at,omitempty"`
+ ID uint64 `json:"id"`
+ UUID string `json:"uuid"`
+ ObjectUUID string `json:"object_uuid"`
+ ObjectOwnerUUID string `json:"object_owner_uuid"`
+ EventType string `json:"event_type"`
+ EventAt *time.Time `json:"event"`
+ Properties map[string]interface{} `json:"properties"`
+ CreatedAt *time.Time `json:"created_at"`
}
// LogList is an arvados#logList resource.
Domain string `json:"domain"`
Hostname string `json:"hostname"`
IPAddress string `json:"ip_address"`
- LastPingAt *time.Time `json:"last_ping_at,omitempty"`
+ LastPingAt *time.Time `json:"last_ping_at"`
SlotNumber int `json:"slot_number"`
Status string `json:"status"`
- JobUUID string `json:"job_uuid,omitempty"`
+ JobUUID string `json:"job_uuid"`
Properties NodeProperties `json:"properties"`
}
package arvados
-import "encoding/json"
+import (
+ "encoding/json"
+ "fmt"
+)
// ResourceListParams expresses which results are requested in a
// list/index API.
Operand interface{}
}
-// MarshalJSON encodes a Filter in the form expected by the API.
+// MarshalJSON encodes a Filter to a JSON array.
func (f *Filter) MarshalJSON() ([]byte, error) {
return json.Marshal([]interface{}{f.Attr, f.Operator, f.Operand})
}
+
+// UnmarshalJSON decodes a JSON array to a Filter.
+func (f *Filter) UnmarshalJSON(data []byte) error {
+ var elements []interface{}
+ err := json.Unmarshal(data, &elements)
+ if err != nil {
+ return err
+ }
+ if len(elements) != 3 {
+ return fmt.Errorf("invalid filter %q: must have 3 elements", data)
+ }
+ attr, ok := elements[0].(string)
+ if !ok {
+ return fmt.Errorf("invalid filter attr %q", elements[0])
+ }
+ op, ok := elements[1].(string)
+ if !ok {
+ return fmt.Errorf("invalid filter operator %q", elements[1])
+ }
+ operand := elements[2]
+ switch operand.(type) {
+ case string, float64, []interface{}, nil:
+ default:
+ return fmt.Errorf("invalid filter operand %q", elements[2])
+ }
+ *f = Filter{attr, op, operand}
+ return nil
+}
t.Errorf("Encoded as %q, expected %q", buf, expect)
}
}
+
+func TestMarshalFiltersWithNil(t *testing.T) {
+ buf, err := json.Marshal([]Filter{
+ {Attr: "modified_at", Operator: "=", Operand: nil}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if expect := []byte(`[["modified_at","=",null]]`); 0 != bytes.Compare(buf, expect) {
+ t.Errorf("Encoded as %q, expected %q", buf, expect)
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "time"
+
+type Specimen struct {
+ UUID string `json:"uuid"`
+ OwnerUUID string `json:"owner_uuid"`
+ CreatedAt time.Time `json:"created_at"`
+ ModifiedAt time.Time `json:"modified_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ Properties map[string]interface{} `json:"properties"`
+}
+
+type SpecimenList struct {
+ Items []Specimen `json:"items"`
+ ItemsAvailable int `json:"items_available"`
+ Offset int `json:"offset"`
+ Limit int `json:"limit"`
+}
// User is an arvados#user record
type User struct {
- UUID string `json:"uuid,omitempty"`
+ UUID string `json:"uuid"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
- Username string `json:"username,omitempty"`
- Email string `json:"email,omitempty"`
+ Username string `json:"username"`
+ Email string `json:"email"`
}
// UserList is an arvados#userList resource.
// Workflow is an arvados#workflow resource.
type Workflow struct {
- UUID string `json:"uuid,omitempty"`
- OwnerUUID string `json:"owner_uuid,omitempty"`
- Name string `json:"name,omitempty"`
- Description string `json:"description,omitempty"`
- Definition string `json:"definition,omitempty"`
- CreatedAt *time.Time `json:"created_at,omitempty"`
- ModifiedAt *time.Time `json:"modified_at,omitempty"`
+ UUID string `json:"uuid"`
+ OwnerUUID string `json:"owner_uuid"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Definition string `json:"definition"`
+ CreatedAt *time.Time `json:"created_at"`
+ ModifiedAt *time.Time `json:"modified_at"`
}
// WorkflowList is an arvados#workflowList resource.
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import (
+ "context"
+ "errors"
+ "sync"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+var ErrStubUnimplemented = errors.New("stub unimplemented")
+
+type APIStub struct {
+ // The error to return from every stubbed API method.
+ Error error
+ calls []APIStubCall
+ mtx sync.Mutex
+}
+
+func (as *APIStub) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionCreate, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionUpdate, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionGet, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+ as.appendCall(as.CollectionList, ctx, options)
+ return arvados.CollectionList{}, as.Error
+}
+func (as *APIStub) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ as.appendCall(as.CollectionProvenance, ctx, options)
+ return nil, as.Error
+}
+func (as *APIStub) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+ as.appendCall(as.CollectionUsedBy, ctx, options)
+ return nil, as.Error
+}
+func (as *APIStub) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionDelete, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionTrash, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+ as.appendCall(as.CollectionUntrash, ctx, options)
+ return arvados.Collection{}, as.Error
+}
+func (as *APIStub) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerCreate, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerUpdate, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerGet, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+ as.appendCall(as.ContainerList, ctx, options)
+ return arvados.ContainerList{}, as.Error
+}
+func (as *APIStub) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerDelete, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerLock, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+ as.appendCall(as.ContainerUnlock, ctx, options)
+ return arvados.Container{}, as.Error
+}
+func (as *APIStub) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+ as.appendCall(as.SpecimenCreate, ctx, options)
+ return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+ as.appendCall(as.SpecimenUpdate, ctx, options)
+ return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+ as.appendCall(as.SpecimenGet, ctx, options)
+ return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+ as.appendCall(as.SpecimenList, ctx, options)
+ return arvados.SpecimenList{}, as.Error
+}
+func (as *APIStub) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+ as.appendCall(as.SpecimenDelete, ctx, options)
+ return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+ as.appendCall(as.APIClientAuthorizationCurrent, ctx, options)
+ return arvados.APIClientAuthorization{}, as.Error
+}
+
+func (as *APIStub) appendCall(method interface{}, ctx context.Context, options interface{}) {
+ as.mtx.Lock()
+ defer as.mtx.Unlock()
+ as.calls = append(as.calls, APIStubCall{method, ctx, options})
+}
+
+func (as *APIStub) Calls(method interface{}) []APIStubCall {
+ as.mtx.Lock()
+ defer as.mtx.Unlock()
+ var calls []APIStubCall
+ for _, call := range as.calls {
+ if method == nil || call.Method == method {
+ calls = append(calls, call)
+ }
+ }
+ return calls
+}
+
+type APIStubCall struct {
+ Method interface{}
+ Context context.Context
+ Options interface{}
+}
ASubprojectUUID = "zzzzz-j7d0g-axqo7eu9pwvna1x"
FooAndBarFilesInDirUUID = "zzzzz-4zz18-foonbarfilesdir"
- FooAndBarFilesInDirPDH = "6bbac24198d09a93975f60098caf0bdf+62"
+ FooAndBarFilesInDirPDH = "870369fc72738603c2fad16664e50e2d+58"
Dispatch1Token = "kwi8oowusvbutahacwk2geulqewy5oaqmpalczfna4b6bb0hfw"
Dispatch1AuthUUID = "zzzzz-gj3su-k9dvestay1plssr"
FooCollectionSharingToken = "iknqgmunrhgsyfok8uzjlwun9iscwm3xacmzmg65fa1j1lpdss"
WorkflowWithDefinitionYAMLUUID = "zzzzz-7fd4e-validworkfloyml"
+
+ CollectionReplicationDesired2Confirmed2UUID = "zzzzz-4zz18-434zv1tnnf2rygp"
)
// PathologicalManifest : A valid manifest designed to test
package auth
import (
+ "context"
"encoding/base64"
"net/http"
"net/url"
return &Credentials{Tokens: []string{}}
}
+func NewContext(ctx context.Context, c *Credentials) context.Context {
+ return context.WithValue(ctx, contextKeyCredentials{}, c)
+}
+
+func FromContext(ctx context.Context) (*Credentials, bool) {
+ c, ok := ctx.Value(contextKeyCredentials{}).(*Credentials)
+ return c, ok
+}
+
func CredentialsFromRequest(r *http.Request) *Credentials {
- if c, ok := r.Context().Value(contextKeyCredentials).(*Credentials); ok {
+ if c, ok := FromContext(r.Context()); ok {
// preloaded by middleware
return c
}
"net/http"
)
-type contextKey string
-
-var contextKeyCredentials contextKey = "credentials"
+type contextKeyCredentials struct{}
// LoadToken wraps the next handler, adding credentials to the request
// context so subsequent handlers can access them efficiently via
// CredentialsFromRequest.
func LoadToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if _, ok := r.Context().Value(contextKeyCredentials).(*Credentials); !ok {
- r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials, CredentialsFromRequest(r)))
+ if _, ok := r.Context().Value(contextKeyCredentials{}).(*Credentials); !ok {
+ r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials{}, CredentialsFromRequest(r)))
}
next.ServeHTTP(w, r)
})
}
func Error(w http.ResponseWriter, error string, code int) {
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("X-Content-Type-Options", "nosniff")
- w.WriteHeader(code)
- json.NewEncoder(w).Encode(ErrorResponse{Errors: []string{error}})
+ Errors(w, []string{error}, code)
}
func Errors(w http.ResponseWriter, errors []string, code int) {
"net/http"
"time"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
"git.curoverse.com/arvados.git/sdk/go/stats"
"github.com/sirupsen/logrus"
)
var (
requestTimeContextKey = contextKey{"requestTime"}
- loggerContextKey = contextKey{"logger"}
)
+// HandlerWithContext returns an http.Handler that changes the request
+// context to ctx (replacing http.Server's default
+// context.Background()), then calls next.
+func HandlerWithContext(ctx context.Context, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
// LogRequests wraps an http.Handler, logging each request and
-// response via logger.
-func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
- if logger == nil {
- logger = logrus.StandardLogger()
- }
+// response.
+func LogRequests(h http.Handler) http.Handler {
return http.HandlerFunc(func(wrapped http.ResponseWriter, req *http.Request) {
w := &responseTimer{ResponseWriter: WrapResponseWriter(wrapped)}
- lgr := logger.WithFields(logrus.Fields{
+ lgr := ctxlog.FromContext(req.Context()).WithFields(logrus.Fields{
"RequestID": req.Header.Get("X-Request-Id"),
"remoteAddr": req.RemoteAddr,
"reqForwardedFor": req.Header.Get("X-Forwarded-For"),
})
ctx := req.Context()
ctx = context.WithValue(ctx, &requestTimeContextKey, time.Now())
- ctx = context.WithValue(ctx, &loggerContextKey, lgr)
+ ctx = ctxlog.Context(ctx, lgr)
req = req.WithContext(ctx)
logRequest(w, req, lgr)
}
func Logger(req *http.Request) logrus.FieldLogger {
- if lgr, ok := req.Context().Value(&loggerContextKey).(logrus.FieldLogger); ok {
- return lgr
- } else {
- return logrus.StandardLogger()
- }
+ return ctxlog.FromContext(req.Context())
}
func logRequest(w *responseTimer, req *http.Request, lgr *logrus.Entry) {
import (
"bytes"
+ "context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
"github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
log.Formatter = &logrus.JSONFormatter{
TimestampFormat: time.RFC3339Nano,
}
+ ctx := ctxlog.Context(context.Background(), log)
+
+ h := AddRequestIDs(LogRequests(
+ http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ w.Write([]byte("hello world"))
+ })))
- h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello world"))
- })
req, err := http.NewRequest("GET", "https://foo.example/bar", nil)
req.Header.Set("X-Forwarded-For", "1.2.3.4:12345")
c.Assert(err, check.IsNil)
resp := httptest.NewRecorder()
- AddRequestIDs(LogRequests(log, h)).ServeHTTP(resp, req)
+
+ HandlerWithContext(ctx, h).ServeHTTP(resp, req)
dec := json.NewDecoder(captured)
//
// For the metrics to be accurate, the caller must ensure every
// request passed to the Handler also passes through
-// LogRequests(logger, ...), and vice versa.
+// LogRequests(...), and vice versa.
//
// If registry is nil, a new registry is created.
//
// It's not safe to copy *http.DefaultTransport
// because it has a mutex (which might be locked)
// protecting a private map (which might not be nil).
- // So we build our own, using the Go 1.10 default
+ // So we build our own, using the Go 1.12 default
// values, ignoring any changes the application has
// made to http.DefaultTransport.
Transport: &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: tlsTimeout,
- ExpectContinueTimeout: time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: arvadosclient.MakeTLSConfig(kc.Arvados.ApiInsecure),
},
}
}
http {
log_format customlog
- '[$time_local] $server_name $status $body_bytes_sent $request_time $request_method "$scheme://$http_host$request_uri" $remote_addr:$remote_port '
+ '[$time_local] "$http_x_request_id" $server_name $status $body_bytes_sent $request_time $request_method "$scheme://$http_host$request_uri" $remote_addr:$remote_port '
'"$http_referer" "$http_user_agent"';
access_log "{{ACCESSLOG}}" customlog;
client_body_temp_path "{{TMPDIR}}";
f.write("""
Clusters:
zzzzz:
+ EnableBetaController14287: {beta14287}
ManagementToken: e687950a23c3a9bceec28c6223a06c79
API:
RequestTimeout: 30s
+ Logging:
+ Level: "{loglevel}"
+ HTTPRequestTimeout: 30s
PostgreSQL:
ConnectionPool: 32
Connection:
InternalURLs:
"https://localhost:{railsport}": {{}}
""".format(
+ beta14287=('true' if '14287' in os.environ.get('ARVADOS_EXPERIMENTAL', '') else 'false'),
+ loglevel=('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
dbhost=_dbconfig('host'),
dbname=_dbconfig('database'),
dbuser=_dbconfig('username'),
before_validation :check_manifest_validity
before_validation :check_signatures
before_validation :strip_signatures_and_update_replication_confirmed
+ before_validation :name_null_if_empty
validate :ensure_pdh_matches_manifest_text
validate :ensure_storage_classes_desired_is_not_empty
validate :ensure_storage_classes_contain_non_empty_strings
around_update :manage_versioning, unless: :is_past_version?
api_accessible :user, extend: :common do |t|
- t.add :name
+ t.add lambda { |x| x.name || "" }, as: :name
t.add :description
t.add :properties
t.add :portable_data_hash
# correct timestamp in signed_manifest_text.
'manifest_text' => ['manifest_text', 'trash_at', 'is_trashed'],
'unsigned_manifest_text' => ['manifest_text'],
+ 'name' => ['name'],
)
end
end
end
+ def name_null_if_empty
+ if name == ""
+ self.name = nil
+ end
+ end
+
def set_file_names
if self.manifest_text_changed?
self.file_names = manifest_files
w_a_z_file:
uuid: zzzzz-4zz18-25k12570yk134b3
current_version_uuid: zzzzz-4zz18-25k12570yk134b3
- portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
+ portable_data_hash: 44a8da9ec82098323895cd14e178386f+56
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2015-02-09T10:53:38Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
multilevel_collection_1:
uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
current_version_uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
- portable_data_hash: 1fd08fc162a5c6413070a8bd0bffc818+150
+ portable_data_hash: f9ddda46bb293b6847da984e3aa735db+290
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
current_version_uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
# All of this collection's files are deep in subdirectories.
- portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ portable_data_hash: 8591cc5caeca80fc62fd529ba1d63bf3+118
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
unique_expired_collection2:
uuid: zzzzz-4zz18-mto52zx1s7sn3jr
current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jr
- portable_data_hash: 4ad199f90029935844dc3f098f4fca2b+49
+ portable_data_hash: 64a2bed1ef0f40fe3a7d39bcf2584cb8+50
owner_uuid: zzzzz-tpzed-000000000000000
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
is_trashed: true
trash_at: 2001-01-01T00:00:00Z
delete_at: 2038-01-01T00:00:00Z
- manifest_text: ". 29d7797f1888013986899bc9083783fa+3 0:3:expired\n"
+ manifest_text: ". 29d7797f1888013986899bc9083783fa+3 0:3:expired2\n"
name: unique_expired_collection2
# a collection with a log file that can be parsed by the log viewer
uuid: zzzzz-4zz18-filesinsubdir00
current_version_uuid: zzzzz-4zz18-filesinsubdir00
name: collection_files_in_subdir
- portable_data_hash: 85877ca2d7e05498dd3d109baf2df106+95
+ portable_data_hash: 7eb64275355980ebc93411b44050c137+281
owner_uuid: zzzzz-tpzed-user1withloadab
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
modified_by_user_uuid: zzzzz-tpzed-user1withloadab
modified_at: 2014-02-03T17:22:54Z
updated_at: 2014-02-03T17:22:54Z
- manifest_text: ". 85877ca2d7e05498dd3d109baf2df106+95 0:95:file_in_subdir1\n./subdir2/subdir3 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir3.txt 32:32:file2_in_subdir3.txt\n./subdir2/subdir3/subdir4 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir4.txt 32:32:file2_in_subdir4.txt"
+ manifest_text: ". 85877ca2d7e05498dd3d109baf2df106+95 0:95:file_in_subdir1\n./subdir2/subdir3 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir3.txt 32:32:file2_in_subdir3.txt\n./subdir2/subdir3/subdir4 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir4.txt 32:32:file2_in_subdir4.txt\n"
graph_test_collection1:
uuid: zzzzz-4zz18-bv31uwvy3neko22
collection_with_repeated_filenames_and_contents_in_two_dirs_1:
uuid: zzzzz-4zz18-duplicatenames1
current_version_uuid: zzzzz-4zz18-duplicatenames1
- portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
+ portable_data_hash: ce437b12aa73ab34f7af5227f556c9e6+142
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
foo_and_bar_files_in_dir:
uuid: zzzzz-4zz18-foonbarfilesdir
current_version_uuid: zzzzz-4zz18-foonbarfilesdir
- portable_data_hash: 6bbac24198d09a93975f60098caf0bdf+62
+ portable_data_hash: 870369fc72738603c2fad16664e50e2d+58
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
collection_not_readable_by_active:
uuid: zzzzz-4zz18-cd42uwvy3neko21
current_version_uuid: zzzzz-4zz18-cd42uwvy3neko21
- portable_data_hash: bb89eb5140e2848d39b416daeef4ffc5+45
+ portable_data_hash: b9e51a238ce08a698e7d7f8f101aee18+55
owner_uuid: zzzzz-tpzed-000000000000000
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
modified_at: 2014-02-03T17:22:54Z
updated_at: 2014-02-03T17:22:54Z
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar 0:0:empty\n"
name: collection_not_readable_by_active
collection_to_remove_and_rename_files:
uuid: zzzzz-4zz18-a21ux3541sxa8sf
current_version_uuid: zzzzz-4zz18-a21ux3541sxa8sf
- portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
trashed_collection_to_test_name_conflict_on_untrash:
uuid: zzzzz-4zz18-trashedcolnamec
current_version_uuid: zzzzz-4zz18-trashedcolnamec
- portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
uuid: zzzzz-4zz18-namesameastrash
current_version_uuid: zzzzz-4zz18-namesameastrash
- portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
collection_in_trashed_subproject:
uuid: zzzzz-4zz18-trashedproj2col
current_version_uuid: zzzzz-4zz18-trashedproj2col
- portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
owner_uuid: zzzzz-j7d0g-trashedproject2
created_at: 2014-02-03T17:22:54Z
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
basename: bar
class: File
location: "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/bar"
- /var/lib/cwl/workflow.json: "keep:1fd08fc162a5c6413070a8bd0bffc818+150"
+ /var/lib/cwl/workflow.json: "keep:f9ddda46bb293b6847da984e3aa735db+290"
uncommitted:
uuid: zzzzz-xvhdp-cr4uncommittedc
assert_empty Collection.where(uuid: uuid)
end
+ test "empty names are exempt from name uniqueness" do
+ act_as_user users(:active) do
+ c1 = Collection.new(name: nil, manifest_text: '', owner_uuid: groups(:aproject).uuid)
+ assert c1.save
+ c2 = Collection.new(name: '', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+ assert c2.save
+ c3 = Collection.new(name: '', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+ assert c3.save
+ c4 = Collection.new(name: 'c4', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+ assert c4.save
+ c5 = Collection.new(name: 'c4', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ c5.save
+ end
+ end
+ end
+
test "create collections with managed properties" do
Rails.configuration.Collections.ManagedProperties = {
'default_prop1' => {'Value' => 'prop1_value'},
go func() {
_, err := io.Copy(response.Conn, stdinRdr)
if err != nil {
- runner.CrunchLog.Printf("While writing stdin collection to docker container %q", err)
+ runner.CrunchLog.Printf("While writing stdin collection to docker container: %v", err)
runner.stop(nil)
}
stdinRdr.Close()
go func() {
_, err := io.Copy(response.Conn, bytes.NewReader(stdinJson))
if err != nil {
- runner.CrunchLog.Printf("While writing stdin json to docker container %q", err)
+ runner.CrunchLog.Printf("While writing stdin json to docker container: %v", err)
runner.stop(nil)
}
response.CloseWrite()
var lastMod time.Time
sawUUID := make(map[string]bool)
err := EachCollection(&s.config.Client, pageSize, func(c arvados.Collection) error {
- got[trial] = append(got[trial], c.UUID)
if c.ModifiedAt == nil {
return nil
}
// dup
return nil
}
+ got[trial] = append(got[trial], c.UUID)
sawUUID[c.UUID] = true
if lastMod == *c.ModifiedAt {
streak++
package main
import (
+ "context"
"fmt"
"net/http"
"os"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/auth"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"github.com/sirupsen/logrus"
)
if srv.config.Listen == "" {
return nil
}
+ ctx := ctxlog.Context(context.Background(), srv.Logger)
server := &httpserver.Server{
Server: http.Server{
- Handler: httpserver.LogRequests(srv.Logger,
- auth.RequireLiteralToken(srv.config.ManagementToken,
- srv.metrics.Handler(srv.Logger))),
+ Handler: httpserver.HandlerWithContext(ctx,
+ httpserver.LogRequests(
+ auth.RequireLiteralToken(srv.config.ManagementToken,
+ srv.metrics.Handler(srv.Logger)))),
},
Addr: srv.config.Listen,
}
}
var updated arvados.Collection
defer c.pdhs.Remove(coll.UUID)
- err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, client.UpdateBody(coll), nil)
+ err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
+ "collection": map[string]string{
+ "manifest_text": coll.ManifestText,
+ },
+ })
if err == nil {
c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
expire: time.Now().Add(time.Duration(c.TTL)),
"fmt"
"io"
"io/ioutil"
- "net/url"
"os"
"os/exec"
"path/filepath"
var newCollection arvados.Collection
arv := arvados.NewClientFromEnv()
arv.AuthToken = arvadostest.ActiveToken
- err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", bytes.NewBufferString(url.Values{"collection": {"{}"}}.Encode()), nil)
+ err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{}})
c.Assert(err, check.IsNil)
readPath, writePath, pdhPath := pathFunc(newCollection)
f.Close()
mtxt, err := fs.MarshalManifest(".")
c.Assert(err, check.IsNil)
- coll := arvados.Collection{ManifestText: mtxt}
- err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", client.UpdateBody(coll), nil)
+ var coll arvados.Collection
+ err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+ "collection": map[string]string{
+ "manifest_text": mtxt,
+ },
+ })
c.Assert(err, check.IsNil)
u, _ := url.Parse("http://download.example.com/c=" + coll.UUID + "/")
func (s *IntegrationSuite) TestDeleteLastFile(c *check.C) {
arv := arvados.NewClientFromEnv()
var newCollection arvados.Collection
- err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", arv.UpdateBody(&arvados.Collection{
- OwnerUUID: arvadostest.ActiveUserUUID,
- ManifestText: ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
- Name: "keep-web test collection",
- }), map[string]bool{"ensure_unique_name": true})
+ err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+ "collection": map[string]string{
+ "owner_uuid": arvadostest.ActiveUserUUID,
+ "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
+ "name": "keep-web test collection",
+ },
+ "ensure_unique_name": true,
+ })
c.Assert(err, check.IsNil)
defer arv.RequestAndDecode(&newCollection, "DELETE", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
package main
import (
+ "context"
"net/http"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"github.com/prometheus/client_golang/prometheus"
+ "github.com/sirupsen/logrus"
)
type server struct {
h := &handler{Config: srv.Config}
reg := prometheus.NewRegistry()
h.Config.Cache.registry = reg
- mh := httpserver.Instrument(reg, nil, httpserver.AddRequestIDs(httpserver.LogRequests(nil, h)))
+ ctx := ctxlog.Context(context.Background(), logrus.StandardLogger())
+ mh := httpserver.Instrument(reg, nil, httpserver.HandlerWithContext(ctx, httpserver.AddRequestIDs(httpserver.LogRequests(h))))
h.MetricsAPI = mh.ServeAPI(h.Config.ManagementToken, http.NotFoundHandler())
srv.Handler = mh
srv.Addr = srv.Config.Listen
// Start serving requests.
router = MakeRESTRouter(!cfg.DisableGet, !cfg.DisablePut, kc, time.Duration(cfg.Timeout), cfg.ManagementToken)
- http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(nil, router)))
+ http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(router)))
log.Println("shutting down")
}
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
"git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"github.com/gorilla/mux"
rtr.metrics.setupWorkQueueMetrics(trashq, "trash")
rtr.metrics.setupRequestMetrics(rtr.limiter)
- instrumented := httpserver.Instrument(rtr.metrics.reg, nil,
- httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter)))
+ instrumented := httpserver.Instrument(rtr.metrics.reg, log,
+ httpserver.HandlerWithContext(
+ ctxlog.Context(context.Background(), log),
+ httpserver.AddRequestIDs(httpserver.LogRequests(rtr.limiter))))
return instrumented.ServeAPI(theConfig.ManagementToken, instrumented)
}
ac := arvados.NewClientFromEnv()
ac.AuthToken = s.token
- coll := &arvados.Collection{ManifestText: ""}
- err := ac.RequestAndDecode(coll, "POST", "arvados/v1/collections", s.jsonBody("collection", coll), map[string]interface{}{"ensure_unique_name": true})
+ var coll arvados.Collection
+ err := ac.RequestAndDecode(&coll, "POST", "arvados/v1/collections", s.jsonBody("collection", `{"manifest_text":""}`), map[string]interface{}{"ensure_unique_name": true})
c.Assert(err, check.IsNil)
s.ignoreLogID = s.lastLogID(c)
wf := &arvados.Workflow{
Name: "ws_test",
}
- err := ac.RequestAndDecode(wf, "POST", "arvados/v1/workflows", s.jsonBody("workflow", wf), map[string]interface{}{"ensure_unique_name": true})
+ err := ac.RequestAndDecode(wf, "POST", "arvados/v1/workflows", s.jsonBody("workflow", `{"name":"ws_test"}`), map[string]interface{}{"ensure_unique_name": true})
if err != nil {
panic(err)
}
uuidChan <- wf.UUID
}
lg := &arvados.Log{}
- err = ac.RequestAndDecode(lg, "POST", "arvados/v1/logs", s.jsonBody("log", &arvados.Log{
- ObjectUUID: wf.UUID,
- EventType: "blip",
- Properties: map[string]interface{}{
+ err = ac.RequestAndDecode(lg, "POST", "arvados/v1/logs", s.jsonBody("log", map[string]interface{}{
+ "object_uuid": wf.UUID,
+ "event_type": "blip",
+ "properties": map[string]interface{}{
"beep": "boop",
},
}), nil)
if err != nil {
panic(err)
}
- err = ac.RequestAndDecode(wf, "PUT", "arvados/v1/workflows/"+wf.UUID, s.jsonBody("workflow", wf), nil)
+ err = ac.RequestAndDecode(wf, "PUT", "arvados/v1/workflows/"+wf.UUID, s.jsonBody("workflow", `{"name":"ws_test"}`), nil)
if err != nil {
panic(err)
}
}
func (s *v0Suite) jsonBody(rscName string, ob interface{}) io.Reader {
- j, err := json.Marshal(ob)
- if err != nil {
- panic(err)
+ val, ok := ob.(string)
+ if !ok {
+ j, err := json.Marshal(ob)
+ if err != nil {
+ panic(err)
+ }
+ val = string(j)
}
v := url.Values{}
- v[rscName] = []string{string(j)}
+ v[rscName] = []string{val}
return bytes.NewBufferString(v.Encode())
}