15954: Merge branch 'master'
authorTom Clegg <tom@tomclegg.ca>
Thu, 27 Feb 2020 14:25:20 +0000 (09:25 -0500)
committerTom Clegg <tom@tomclegg.ca>
Thu, 27 Feb 2020 14:25:20 +0000 (09:25 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

22 files changed:
apps/workbench/app/controllers/virtual_machines_controller.rb
doc/api/methods.html.textile.liquid
lib/controller/federation/conn.go
lib/controller/federation/login_test.go
lib/controller/handler.go
lib/controller/handler_test.go
lib/controller/localdb/conn.go
lib/controller/localdb/login.go
lib/controller/localdb/login_test.go
lib/controller/router/router.go
lib/controller/rpc/conn.go
lib/controller/rpc/conn_test.go
sdk/go/arvados/api.go
sdk/go/arvados/login.go
sdk/go/arvadostest/api.go
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/lib/record_filters.rb
services/api/test/fixtures/collections.yml
services/api/test/functional/arvados/v1/filters_test.rb
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/service/nginx/run
tools/arvbox/lib/arvbox/docker/service/sdk/run-service

index 1427e3cc728e3d30d5399223a3c4e311413a9d48..c743773141f672532396e7f73677f602548c41cc 100644 (file)
@@ -25,8 +25,8 @@ class VirtualMachinesController < ApplicationController
   end
 
   def webshell
-    return render_not_found if Rails.configuration.Workbench.ShellInABoxURL == URI("")
-    webshell_url = URI(Rails.configuration.Workbench.ShellInABoxURL)
+    return render_not_found if Rails.configuration.Services.WebShell.ExternalURL == URI("")
+    webshell_url = URI(Rails.configuration.Services.WebShell.ExternalURL)
     if webshell_url.host.index("*") != nil
       webshell_url.host = webshell_url.host.sub("*", @object.hostname)
     else
index e08dbb283709cf1f202552a9ca39673e180f9da4..872a1bca7149acb22f891d243a1be316d4d7a9c8 100644 (file)
@@ -130,6 +130,7 @@ table(table table-bordered table-condensed).
 |@like@, @ilike@|string|SQL pattern match, single character match is @_@ and wildcard is @%@, ilike is case-insensitive|@["properties.my_subproperty", "like", "d00220fb%"]@|
 |@in@, @not in@|array of strings|Set membership|@["properties.my_subproperty", "in", ["fizz", "buzz"]]@|
 |@exists@|boolean|Test if a subproperty is present or not (determined by operand).|@["properties.my_subproperty", "exists", true]@|
+|@contains@|string, number|Filter where subproperty has a value either by exact match or value is element of subproperty list.|@["foo", "contains", "bar"]@ will find both @{"foo": "bar"}@ and @{"foo": ["bar", "baz"]}@.|
 
 Note that exclusion filters @!=@ and @not in@ will return records for which the property is not defined at all.  To restrict filtering to records on which the subproperty is defined, combine with an @exists@ filter.
 
index 3909b6cdd5ab5cad8e8e7b2024606811baf248ca..279b7a51d5d8d4e57f920721db10ace45268b1d2 100644 (file)
@@ -201,6 +201,32 @@ func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arva
        }
 }
 
+func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       // If the logout request comes with an API token from a known
+       // remote cluster, redirect to that cluster's logout handler
+       // so it has an opportunity to clear sessions, expire tokens,
+       // etc. Otherwise use the local endpoint.
+       reqauth, ok := auth.FromContext(ctx)
+       if !ok || len(reqauth.Tokens) == 0 || len(reqauth.Tokens[0]) < 8 || !strings.HasPrefix(reqauth.Tokens[0], "v2/") {
+               return conn.local.Logout(ctx, options)
+       }
+       id := reqauth.Tokens[0][3:8]
+       if id == conn.cluster.ClusterID {
+               return conn.local.Logout(ctx, options)
+       }
+       remote, ok := conn.remotes[id]
+       if !ok {
+               return conn.local.Logout(ctx, options)
+       }
+       baseURL := remote.BaseURL()
+       target, err := baseURL.Parse(arvados.EndpointLogout.Path)
+       if err != nil {
+               return arvados.LogoutResponse{}, fmt.Errorf("internal error getting redirect target: %s", err)
+       }
+       target.RawQuery = url.Values{"return_to": {options.ReturnTo}}.Encode()
+       return arvados.LogoutResponse{RedirectLocation: target.String()}, nil
+}
+
 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
        if len(options.UUID) == 27 {
                // UUID is really a UUID
index ab39619c79703ff683a4adc4e16396cde2d3ebc5..1d6e12e0159f5c73d45ebfda595a10587580a9b5 100644 (file)
@@ -10,6 +10,7 @@ import (
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/auth"
        check "gopkg.in/check.v1"
 )
 
@@ -38,3 +39,36 @@ func (s *LoginSuite) TestDeferToLoginCluster(c *check.C) {
                c.Check(remotePresent, check.Equals, remote != "")
        }
 }
+
+func (s *LoginSuite) TestLogout(c *check.C) {
+       s.cluster.Services.Workbench1.ExternalURL = arvados.URL{Scheme: "https", Host: "workbench1.example.com"}
+       s.cluster.Services.Workbench2.ExternalURL = arvados.URL{Scheme: "https", Host: "workbench2.example.com"}
+       s.cluster.Login.GoogleClientID = "zzzzzzzzzzzzzz"
+       s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
+       s.cluster.Login.LoginCluster = "zhome"
+
+       returnTo := "https://app.example.com/foo?bar"
+       for _, trial := range []struct {
+               token    string
+               returnTo string
+               target   string
+       }{
+               {token: "", returnTo: "", target: s.cluster.Services.Workbench2.ExternalURL.String()},
+               {token: "", returnTo: returnTo, target: returnTo},
+               {token: "zzzzzzzzzzzzzzzzzzzzz", returnTo: returnTo, target: returnTo},
+               {token: "v2/zzzzz-aaaaa-aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", returnTo: returnTo, target: returnTo},
+               {token: "v2/zhome-aaaaa-aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", returnTo: returnTo, target: "http://" + s.cluster.RemoteClusters["zhome"].Host + "/logout?" + url.Values{"return_to": {returnTo}}.Encode()},
+       } {
+               c.Logf("trial %#v", trial)
+               ctx := context.Background()
+               if trial.token != "" {
+                       ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{trial.token}})
+               }
+               resp, err := s.fed.Logout(ctx, arvados.LogoutOptions{ReturnTo: trial.returnTo})
+               c.Assert(err, check.IsNil)
+               c.Logf("  RedirectLocation %q", resp.RedirectLocation)
+               target, err := url.Parse(resp.RedirectLocation)
+               c.Check(err, check.IsNil)
+               c.Check(target.String(), check.Equals, trial.target)
+       }
+}
index 935a1b6cb683a57a2da99b4d4f7285b1bc1ae7f5..e3869261a160b4e42d147574410f15b9641b4e9d 100644 (file)
@@ -86,6 +86,7 @@ func (h *Handler) setup() {
                mux.Handle("/arvados/v1/users", rtr)
                mux.Handle("/arvados/v1/users/", rtr)
                mux.Handle("/login", rtr)
+               mux.Handle("/logout", rtr)
        }
 
        hs := http.NotFoundHandler()
index d54f50cf1ff2a8b9f52c3149548e74257443dbb8..f09203f72486739d467b88f28b1d6875bc2f1959 100644 (file)
@@ -19,6 +19,7 @@ import (
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
        "github.com/prometheus/client_golang/prometheus"
@@ -180,6 +181,32 @@ func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
        c.Check(resp.Header().Get("Location"), check.Matches, `(https://0.0.0.0:1)?/auth/joshid\?return_to=%2Cfoo&?`)
 }
 
+func (s *HandlerSuite) TestLogoutSSO(c *check.C) {
+       s.cluster.Login.ProviderAppID = "test"
+       req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://example.com/foo", nil)
+       resp := httptest.NewRecorder()
+       s.handler.ServeHTTP(resp, req)
+       if !c.Check(resp.Code, check.Equals, http.StatusFound) {
+               c.Log(resp.Body.String())
+       }
+       c.Check(resp.Header().Get("Location"), check.Equals, "http://localhost:3002/users/sign_out?"+url.Values{"redirect_uri": {"https://example.com/foo"}}.Encode())
+}
+
+func (s *HandlerSuite) TestLogoutGoogle(c *check.C) {
+       if s.cluster.ForceLegacyAPI14 {
+               // Google login N/A
+               return
+       }
+       s.cluster.Login.GoogleClientID = "test"
+       req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://example.com/foo", nil)
+       resp := httptest.NewRecorder()
+       s.handler.ServeHTTP(resp, req)
+       if !c.Check(resp.Code, check.Equals, http.StatusFound) {
+               c.Log(resp.Body.String())
+       }
+       c.Check(resp.Header().Get("Location"), check.Equals, "https://example.com/foo")
+}
+
 func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
        user, ok, err := s.handler.(*Handler).validateAPItoken(req, arvadostest.ActiveToken)
@@ -203,6 +230,26 @@ func (s *HandlerSuite) TestValidateV2APIToken(c *check.C) {
        c.Check(user.Authorization.TokenV2(), check.Equals, arvadostest.ActiveTokenV2)
 }
 
+func (s *HandlerSuite) TestValidateRemoteToken(c *check.C) {
+       saltedToken, err := auth.SaltToken(arvadostest.ActiveTokenV2, "abcde")
+       c.Assert(err, check.IsNil)
+       for _, trial := range []struct {
+               code  int
+               token string
+       }{
+               {http.StatusOK, saltedToken},
+               {http.StatusUnauthorized, "bogus"},
+       } {
+               req := httptest.NewRequest("GET", "https://0.0.0.0:1/arvados/v1/users/current?remote=abcde", nil)
+               req.Header.Set("Authorization", "Bearer "+trial.token)
+               resp := httptest.NewRecorder()
+               s.handler.ServeHTTP(resp, req)
+               if !c.Check(resp.Code, check.Equals, trial.code) {
+                       c.Logf("HTTP %d: %s", resp.Code, resp.Body.String())
+               }
+       }
+}
+
 func (s *HandlerSuite) TestCreateAPIToken(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
        auth, err := s.handler.(*Handler).createAPItoken(req, arvadostest.ActiveUserUUID, nil)
index 4139b270c51dac1f51d48dde4880c7e201e2d879..ac092382d42a20c6ec4caad4033ed4a5679d7632 100644 (file)
@@ -29,6 +29,15 @@ func NewConn(cluster *arvados.Cluster) *Conn {
        }
 }
 
+func (conn *Conn) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       if conn.cluster.Login.ProviderAppID != "" {
+               // Proxy to RailsAPI, which hands off to sso-provider.
+               return conn.railsProxy.Logout(ctx, opts)
+       } else {
+               return conn.googleLoginController.Logout(ctx, conn.cluster, conn.railsProxy, opts)
+       }
+}
+
 func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
        wantGoogle := conn.cluster.Login.GoogleClientID != ""
        wantSSO := conn.cluster.Login.ProviderAppID != ""
index b1ebb27e486f91ecda01341d0f09c2e425df5a71..2e50b84f435856dc282be51b4fbe8b5db548431b 100644 (file)
@@ -52,6 +52,18 @@ func (ctrl *googleLoginController) getProvider() (*oidc.Provider, error) {
        return ctrl.provider, nil
 }
 
+func (ctrl *googleLoginController) Logout(ctx context.Context, cluster *arvados.Cluster, railsproxy *railsProxy, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       target := opts.ReturnTo
+       if target == "" {
+               if cluster.Services.Workbench2.ExternalURL.Host != "" {
+                       target = cluster.Services.Workbench2.ExternalURL.String()
+               } else {
+                       target = cluster.Services.Workbench1.ExternalURL.String()
+               }
+       }
+       return arvados.LogoutResponse{RedirectLocation: target}, nil
+}
+
 func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.Cluster, railsproxy *railsProxy, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
        provider, err := ctrl.getProvider()
        if err != nil {
index 9f3267cef0e5c0435a027f73dfa0367afd6d403c..4fb0fbcee18055fbf7f7ac2e6fc688d19fac4cde 100644 (file)
@@ -163,6 +163,12 @@ func (s *LoginSuite) TearDownTest(c *check.C) {
        s.railsSpy.Close()
 }
 
+func (s *LoginSuite) TestGoogleLogout(c *check.C) {
+       resp, err := s.localdb.Logout(context.Background(), arvados.LogoutOptions{ReturnTo: "https://foo.example.com/bar"})
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "https://foo.example.com/bar")
+}
+
 func (s *LoginSuite) TestGoogleLogin_Start_Bogus(c *check.C) {
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{})
        c.Check(err, check.IsNil)
index c41f103dc464be41be43002b24e11a59a9968c8c..69d707703852b7fc60acfcb6e86d8fa960e7a5c9 100644 (file)
@@ -54,6 +54,13 @@ func (rtr *router) addRoutes() {
                                return rtr.fed.Login(ctx, *opts.(*arvados.LoginOptions))
                        },
                },
+               {
+                       arvados.EndpointLogout,
+                       func() interface{} { return &arvados.LogoutOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.Logout(ctx, *opts.(*arvados.LogoutOptions))
+                       },
+               },
                {
                        arvados.EndpointCollectionCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
index bf6166f44ffd649691d317eed302e31bee35b5cf..4b143b770bbbe093bbec7c668f58e2f0100f81a6 100644 (file)
@@ -145,6 +145,14 @@ func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arva
        return resp, err
 }
 
+func (conn *Conn) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       ep := arvados.EndpointLogout
+       var resp arvados.LogoutResponse
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
+       return resp, err
+}
+
 // If the given location is a valid URL and its origin is the same as
 // conn.baseURL, return it as a relative URL. Otherwise, return it
 // unmodified.
index 83a80e878c06de3af7cc252b142423a462391f46..b97c0f87b85f6e4f8c2c0ee798256bde9fced23c 100644 (file)
@@ -51,6 +51,16 @@ func (s *RPCSuite) TestLogin(c *check.C) {
        c.Check(resp.RedirectLocation, check.Equals, "/auth/joshid?return_to="+url.QueryEscape(","+opts.ReturnTo))
 }
 
+func (s *RPCSuite) TestLogout(c *check.C) {
+       s.ctx = context.Background()
+       opts := arvados.LogoutOptions{
+               ReturnTo: "https://foo.example.com/bar",
+       }
+       resp, err := s.conn.Logout(s.ctx, opts)
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "http://localhost:3002/users/sign_out?redirect_uri="+url.QueryEscape(opts.ReturnTo))
+}
+
 func (s *RPCSuite) TestCollectionCreate(c *check.C) {
        coll, err := s.conn.CollectionCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
                "owner_uuid":         arvadostest.ActiveUserUUID,
index 62f9a95fd68cc8be1a1b2b67c14bb2a5c36537c0..0c5d32e8b7e1e96bdab9bbf45b25962963297829 100644 (file)
@@ -19,6 +19,7 @@ type APIEndpoint struct {
 var (
        EndpointConfigGet                     = APIEndpoint{"GET", "arvados/v1/config", ""}
        EndpointLogin                         = APIEndpoint{"GET", "login", ""}
+       EndpointLogout                        = APIEndpoint{"GET", "logout", ""}
        EndpointCollectionCreate              = APIEndpoint{"POST", "arvados/v1/collections", "collection"}
        EndpointCollectionUpdate              = APIEndpoint{"PATCH", "arvados/v1/collections/{uuid}", "collection"}
        EndpointCollectionGet                 = APIEndpoint{"GET", "arvados/v1/collections/{uuid}", ""}
@@ -141,9 +142,14 @@ type LoginOptions struct {
        State    string `json:"state,omitempty"`  // OAuth2 callback state
 }
 
+type LogoutOptions struct {
+       ReturnTo string `json:"return_to"` // Redirect to this URL after logging out
+}
+
 type API interface {
        ConfigGet(ctx context.Context) (json.RawMessage, error)
        Login(ctx context.Context, options LoginOptions) (LoginResponse, error)
+       Logout(ctx context.Context, options LogoutOptions) (LogoutResponse, error)
        CollectionCreate(ctx context.Context, options CreateOptions) (Collection, error)
        CollectionUpdate(ctx context.Context, options UpdateOptions) (Collection, error)
        CollectionGet(ctx context.Context, options GetOptions) (Collection, error)
index 7107ac57ab76c029ccc28764762ad4b08de42bbe..75ebc81c142b1ee167bafa7792923513af3c5120 100644 (file)
@@ -24,3 +24,12 @@ func (resp LoginResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
                w.Write(resp.HTML.Bytes())
        }
 }
+
+type LogoutResponse struct {
+       RedirectLocation string
+}
+
+func (resp LogoutResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+       w.Header().Set("Location", resp.RedirectLocation)
+       w.WriteHeader(http.StatusFound)
+}
index b5cea5c18599afe1ba252e611d7f401ad0c2c622..9019d33cfb8bc167c45cf5e4c1b2fa4107d0c9c3 100644 (file)
@@ -37,6 +37,10 @@ func (as *APIStub) Login(ctx context.Context, options arvados.LoginOptions) (arv
        as.appendCall(as.Login, ctx, options)
        return arvados.LoginResponse{}, as.Error
 }
+func (as *APIStub) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       as.appendCall(as.Logout, ctx, options)
+       return arvados.LogoutResponse{}, as.Error
+}
 func (as *APIStub) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
        as.appendCall(as.CollectionCreate, ctx, options)
        return arvados.Collection{}, as.Error
index aee5d1f9516c038a45fe37f19615eca77e52d44b..5c223410151788926445ac440e20a723ce6cf9aa 100644 (file)
@@ -36,7 +36,7 @@ class Arvados::V1::SchemaController < ApplicationController
         # format is YYYYMMDD, must be fixed with (needs to be linearly
         # sortable), updated manually, may be used by clients to
         # determine availability of API server features.
-        revision: "20190926",
+        revision: "20200212",
         source_version: AppVersion.hash,
         sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
         packageVersion: AppVersion.package_version,
index 994e8503106ad6ec3e931e555f2f28d8d455a1da..5688ca6140f17fcef94ed112481cddb06e75c668 100644 (file)
@@ -19,7 +19,7 @@ module RecordFilters
   # +model_class+    subclass of ActiveRecord being filtered
   #
   # Output:
-  # Hash with two keys:
+  # Hash with the following keys:
   # :cond_out  array of SQL fragments for each filter expression
   # :param_out array of values for parameter substitution in cond_out
   # :joins     array of joins: either [] or ["JOIN containers ON ..."]
@@ -140,6 +140,10 @@ module RecordFilters
               raise ArgumentError.new("Invalid operand '#{operand}' for '#{operator}' must be true or false")
             end
             param_out << proppath
+          when 'contains'
+            cond_out << "#{attr_table_name}.#{attr} @> ?::jsonb OR #{attr_table_name}.#{attr} @> ?::jsonb"
+            param_out << SafeJSON.dump({proppath => operand})
+            param_out << SafeJSON.dump({proppath => [operand]})
           else
             raise ArgumentError.new("Invalid operator for subproperty search '#{operator}'")
           end
index 1503f6bc0147ebde726fb449ee5af734637fdca1..1581039bb388638ee0fc56decc7ce0940ea49f4d 100644 (file)
@@ -953,7 +953,7 @@ collection_with_prop2_1:
   modified_at: 2015-02-13T17:22:54Z
   updated_at: 2015-02-13T17:22:54Z
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-  name: collection with prop1 1
+  name: collection with prop2 1
   properties:
     prop2: 1
 
@@ -968,10 +968,55 @@ collection_with_prop2_5:
   modified_at: 2015-02-13T17:22:54Z
   updated_at: 2015-02-13T17:22:54Z
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-  name: collection with prop1 5
+  name: collection with prop2 5
   properties:
     prop2: 5
 
+collection_with_list_prop_odd:
+  uuid: zzzzz-4zz18-listpropertyodd
+  current_version_uuid: zzzzz-4zz18-listpropertyodd
+  portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2015-02-13T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2015-02-13T17:22:54Z
+  updated_at: 2015-02-13T17:22:54Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  name: collection with list property with odd values
+  properties:
+    listprop: [elem1, elem3, 5]
+
+collection_with_list_prop_even:
+  uuid: zzzzz-4zz18-listpropertyeven
+  current_version_uuid: zzzzz-4zz18-listpropertyeven
+  portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2015-02-13T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2015-02-13T17:22:54Z
+  updated_at: 2015-02-13T17:22:54Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  name: collection with list property with even values
+  properties:
+    listprop: [elem2, 4, elem6, ELEM8]
+
+collection_with_listprop_elem1:
+  uuid: zzzzz-4zz18-listpropelem1
+  current_version_uuid: zzzzz-4zz18-listpropelem1
+  portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2015-02-13T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2015-02-13T17:22:54Z
+  updated_at: 2015-02-13T17:22:54Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  name: collection with list property with string value
+  properties:
+    listprop: elem1
+
 collection_with_uri_prop:
   uuid: zzzzz-4zz18-withuripropval1
   current_version_uuid: zzzzz-4zz18-withuripropval1
index d49fe7a3e647caec2215c6f911fe5e37b1d6a5ab..b30afd745345df01fdb986d8ead8b8c7ad2ab0c4 100644 (file)
@@ -172,6 +172,13 @@ class Arvados::V1::FiltersTest < ActionController::TestCase
    ['prop2', '<=', 5, [:collection_with_prop2_1, :collection_with_prop2_5], []],
    ['prop2', '>=', 1, [:collection_with_prop2_1, :collection_with_prop2_5], []],
    ['<http://schema.org/example>', '=', "value1", [:collection_with_uri_prop], []],
+   ['listprop', 'contains', 'elem1', [:collection_with_list_prop_odd, :collection_with_listprop_elem1], [:collection_with_list_prop_even]],
+   ['listprop', '=', 'elem1', [:collection_with_listprop_elem1], [:collection_with_list_prop_odd]],
+   ['listprop', 'contains', 5, [:collection_with_list_prop_odd], [:collection_with_list_prop_even, :collection_with_listprop_elem1]],
+   ['listprop', 'contains', 'elem2', [:collection_with_list_prop_even], [:collection_with_list_prop_odd, :collection_with_listprop_elem1]],
+   ['listprop', 'contains', 'ELEM2', [], [:collection_with_list_prop_even]],
+   ['listprop', 'contains', 'elem8', [], [:collection_with_list_prop_even]],
+   ['listprop', 'contains', 4, [:collection_with_list_prop_even], [:collection_with_list_prop_odd, :collection_with_listprop_elem1]],
   ].each do |prop, op, opr, inc, ex|
     test "jsonb filter properties.#{prop} #{op} #{opr})" do
       @controller = Arvados::V1::CollectionsController.new
index 77fb3f776030c6498640cdb924807dfaaf651629..9c933e870f375d540aef03742481b0484afa853f 100644 (file)
@@ -88,8 +88,8 @@ pip_install() {
     popd
 
     if [ "$PYCMD" = "python3" ]; then
-       if ! pip3 install --no-index --find-links /var/lib/pip $1 ; then
-            pip3 install $1
+       if ! pip3 install --prefix /usr/local --no-index --find-links /var/lib/pip $1 ; then
+            pip3 install --prefix /usr/local $1
        fi
     else
        if ! pip install --no-index --find-links /var/lib/pip $1 ; then
index 4fcc65fa9755e7a787db4b96d88df7b093b57cc3..d6fecb4436069e431f80682af5baf66f0d04bf82 100755 (executable)
@@ -14,6 +14,11 @@ if [[ $containerip != $localip ]] ; then
     fi
 fi
 
+geo_dockerip=
+if  [[ -f /var/run/localip_override ]] ; then
+    geo_dockerip="$dockerip/32 0;"
+fi
+
 openssl verify -CAfile $root_cert $server_cert
 
 cat <<EOF >/var/lib/arvados/nginx.conf
@@ -38,7 +43,7 @@ http {
       default     1;
       127.0.0.0/8 0;
       $containerip/32 0;
-      $dockerip/32 0;
+      $geo_dockerip
   }
 
   server {
index da6db3653cd9b48b3bea1570aa5006e173c6b319..8a36140bcfef84456e40aea8a3da6ccc63096894 100755 (executable)
@@ -18,6 +18,8 @@ cd /usr/src/arvados/sdk/cli
 run_bundler --binstubs=$PWD/binstubs
 ln -sf /usr/src/arvados/sdk/cli/binstubs/arv /usr/local/bin/arv
 
+export PYCMD=python3
+
 # Need to install the upstream version of pip because the python-pip package
 # shipped with Debian 9 is patched to change behavior in a way that breaks our
 # use case.
@@ -28,7 +30,9 @@ ln -sf /usr/src/arvados/sdk/cli/binstubs/arv /usr/local/bin/arv
 # multiple packages, because it will blindly install the latest version of each
 # dependency requested by each package, even if a compatible package version is
 # already installed.
-pip_install pip==9.0.3
+if ! pip3 install --no-index --find-links /var/lib/pip pip==9.0.3 ; then
+    pip3 install pip==9.0.3
+fi
 
 pip_install wheel