17944: Adds vocabulary checking support to links.
authorLucas Di Pentima <lucas.dipentima@curii.com>
Wed, 3 Nov 2021 15:55:44 +0000 (12:55 -0300)
committerLucas Di Pentima <lucas.dipentima@curii.com>
Wed, 3 Nov 2021 21:40:13 +0000 (18:40 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima@curii.com>

12 files changed:
lib/controller/federation/conn.go
lib/controller/federation/generate.go
lib/controller/federation/generated.go
lib/controller/handler.go
lib/controller/handler_test.go
lib/controller/localdb/link.go [new file with mode: 0644]
lib/controller/localdb/link_test.go [new file with mode: 0644]
lib/controller/router/router.go
lib/controller/rpc/conn.go
sdk/go/arvados/api.go
sdk/go/arvados/link.go
sdk/go/arvadostest/api.go

index 9729416228cd3d7e4ad7667276a843b3086b0e73..d477303527c7b71e827607a24af1ba838ee23464 100644 (file)
@@ -469,6 +469,26 @@ func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptio
        return conn.chooseBackend(options.UUID).GroupUntrash(ctx, options)
 }
 
+func (conn *Conn) LinkCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Link, error) {
+       return conn.chooseBackend(options.ClusterID).LinkCreate(ctx, options)
+}
+
+func (conn *Conn) LinkUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Link, error) {
+       return conn.chooseBackend(options.UUID).LinkUpdate(ctx, options)
+}
+
+func (conn *Conn) LinkGet(ctx context.Context, options arvados.GetOptions) (arvados.Link, error) {
+       return conn.chooseBackend(options.UUID).LinkGet(ctx, options)
+}
+
+func (conn *Conn) LinkList(ctx context.Context, options arvados.ListOptions) (arvados.LinkList, error) {
+       return conn.generated_LinkList(ctx, options)
+}
+
+func (conn *Conn) LinkDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Link, error) {
+       return conn.chooseBackend(options.UUID).LinkDelete(ctx, options)
+}
+
 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
        return conn.generated_SpecimenList(ctx, options)
 }
index 06a5ce12d792ce000d669f513f9b1908ae1011f8..b49e138ce1f635c3b692a873d646da20ca444173 100644 (file)
@@ -53,7 +53,7 @@ func main() {
                defer out.Close()
                out.Write(regexp.MustCompile(`(?ms)^.*package .*?import.*?\n\)\n`).Find(buf))
                io.WriteString(out, "//\n// -- this file is auto-generated -- do not edit -- edit list.go and run \"go generate\" instead --\n//\n\n")
-               for _, t := range []string{"Container", "ContainerRequest", "Group", "Specimen", "User"} {
+               for _, t := range []string{"Container", "ContainerRequest", "Group", "Specimen", "User", "Link"} {
                        _, err := out.Write(bytes.ReplaceAll(orig, []byte("Collection"), []byte(t)))
                        if err != nil {
                                panic(err)
index 49a2e5b7513f537f0f129fac6c8ba81103e72a29..e8a5a08ff00c10d491e5fb29032fe003fb9b137a 100755 (executable)
@@ -221,3 +221,44 @@ func (conn *Conn) generated_UserList(ctx context.Context, options arvados.ListOp
        }
        return merged, err
 }
+
+func (conn *Conn) generated_LinkList(ctx context.Context, options arvados.ListOptions) (arvados.LinkList, error) {
+       var mtx sync.Mutex
+       var merged arvados.LinkList
+       var needSort atomic.Value
+       needSort.Store(false)
+       err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+               options.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor
+               cl, err := backend.LinkList(ctx, options)
+               if err != nil {
+                       return nil, err
+               }
+               mtx.Lock()
+               defer mtx.Unlock()
+               if len(merged.Items) == 0 {
+                       merged = cl
+               } else if len(cl.Items) > 0 {
+                       merged.Items = append(merged.Items, cl.Items...)
+                       needSort.Store(true)
+               }
+               uuids := make([]string, 0, len(cl.Items))
+               for _, item := range cl.Items {
+                       uuids = append(uuids, item.UUID)
+               }
+               return uuids, nil
+       })
+       if needSort.Load().(bool) {
+               // Apply the default/implied order, "modified_at desc"
+               sort.Slice(merged.Items, func(i, j int) bool {
+                       mi, mj := merged.Items[i].ModifiedAt, merged.Items[j].ModifiedAt
+                       return mj.Before(mi)
+               })
+       }
+       if merged.Items == nil {
+               // Return empty results as [], not null
+               // (https://github.com/golang/go/issues/27589 might be
+               // a better solution in the future)
+               merged.Items = []arvados.Link{}
+       }
+       return merged, err
+}
index 6385913078db526ca1b7629222b9c80fdfcf9663..358b0ed0c2f4214e1e09012436372c1f37861391 100644 (file)
@@ -123,6 +123,8 @@ func (h *Handler) setup() {
        mux.Handle("/arvados/v1/container_requests/", rtr)
        mux.Handle("/arvados/v1/groups", rtr)
        mux.Handle("/arvados/v1/groups/", rtr)
+       mux.Handle("/arvados/v1/links", rtr)
+       mux.Handle("/arvados/v1/links/", rtr)
        mux.Handle("/login", rtr)
        mux.Handle("/logout", rtr)
 
index 728c760af2b30852f87fd5c920db7dc5a2e87801..ac27b8ea509e59106e85cfbf58b2d2c8929f8e96 100644 (file)
@@ -343,7 +343,7 @@ func (s *HandlerSuite) CheckObjectType(c *check.C, url string, token string, ski
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
        c.Assert(resp.Code, check.Equals, http.StatusOK,
-               check.Commentf("Wasn't able to get data from the controller at %q", url))
+               check.Commentf("Wasn't able to get data from the controller at %q: %q", url, resp.Body.String()))
        err = json.Unmarshal(resp.Body.Bytes(), &proxied)
        c.Check(err, check.Equals, nil)
 
diff --git a/lib/controller/localdb/link.go b/lib/controller/localdb/link.go
new file mode 100644 (file)
index 0000000..cfcae3d
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+)
+
+// LinkCreate defers to railsProxy for everything except vocabulary
+// checking.
+func (conn *Conn) LinkCreate(ctx context.Context, opts arvados.CreateOptions) (arvados.Link, error) {
+       err := conn.checkProperties(ctx, opts.Attrs["properties"])
+       if err != nil {
+               return arvados.Link{}, err
+       }
+       resp, err := conn.railsProxy.LinkCreate(ctx, opts)
+       if err != nil {
+               return resp, err
+       }
+       return resp, nil
+}
+
+// LinkUpdate defers to railsProxy for everything except vocabulary
+// checking.
+func (conn *Conn) LinkUpdate(ctx context.Context, opts arvados.UpdateOptions) (arvados.Link, error) {
+       err := conn.checkProperties(ctx, opts.Attrs["properties"])
+       if err != nil {
+               return arvados.Link{}, err
+       }
+       resp, err := conn.railsProxy.LinkUpdate(ctx, opts)
+       if err != nil {
+               return resp, err
+       }
+       return resp, nil
+}
diff --git a/lib/controller/localdb/link_test.go b/lib/controller/localdb/link_test.go
new file mode 100644 (file)
index 0000000..05bd473
--- /dev/null
@@ -0,0 +1,143 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "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"
+       check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&LinkSuite{})
+
+type LinkSuite struct {
+       cluster  *arvados.Cluster
+       localdb  *Conn
+       railsSpy *arvadostest.Proxy
+}
+
+func (s *LinkSuite) TearDownSuite(c *check.C) {
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+}
+
+func (s *LinkSuite) SetUpTest(c *check.C) {
+       cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+       c.Assert(err, check.IsNil)
+       s.cluster, err = cfg.GetCluster("")
+       c.Assert(err, check.IsNil)
+       s.localdb = NewConn(s.cluster)
+       s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
+       *s.localdb.railsProxy = *rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
+}
+
+func (s *LinkSuite) TearDownTest(c *check.C) {
+       s.railsSpy.Close()
+}
+
+func (s *LinkSuite) setUpVocabulary(c *check.C, testVocabulary string) {
+       if testVocabulary == "" {
+               testVocabulary = `{
+                       "strict_tags": false,
+                       "tags": {
+                               "IDTAGIMPORTANCES": {
+                                       "strict": true,
+                                       "labels": [{"label": "Importance"}, {"label": "Priority"}],
+                                       "values": {
+                                               "IDVALIMPORTANCES1": { "labels": [{"label": "Critical"}, {"label": "Urgent"}, {"label": "High"}] },
+                                               "IDVALIMPORTANCES2": { "labels": [{"label": "Normal"}, {"label": "Moderate"}] },
+                                               "IDVALIMPORTANCES3": { "labels": [{"label": "Low"}] }
+                                       }
+                               }
+                       }
+               }`
+       }
+       voc, err := arvados.NewVocabulary([]byte(testVocabulary), []string{})
+       c.Assert(err, check.IsNil)
+       c.Assert(voc.Validate(), check.IsNil)
+       s.localdb.vocabularyCache = voc
+       s.cluster.API.VocabularyPath = "foo"
+}
+
+func (s *LinkSuite) TestLinkCreateWithProperties(c *check.C) {
+       s.setUpVocabulary(c, "")
+       ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+
+       tests := []struct {
+               name    string
+               props   map[string]interface{}
+               success bool
+       }{
+               {"Invalid prop key", map[string]interface{}{"Priority": "IDVALIMPORTANCES1"}, false},
+               {"Invalid prop value", map[string]interface{}{"IDTAGIMPORTANCES": "high"}, false},
+               {"Valid prop key & value", map[string]interface{}{"IDTAGIMPORTANCES": "IDVALIMPORTANCES1"}, true},
+               {"Empty properties", map[string]interface{}{}, true},
+       }
+       for _, tt := range tests {
+               c.Log(c.TestName()+" ", tt.name)
+
+               lnk, err := s.localdb.LinkCreate(ctx, arvados.CreateOptions{
+                       Select: []string{"uuid", "properties"},
+                       Attrs: map[string]interface{}{
+                               "link_class": "star",
+                               "tail_uuid":  "zzzzz-j7d0g-publicfavorites",
+                               "head_uuid":  arvadostest.FooCollection,
+                               "properties": tt.props,
+                       }})
+               if tt.success {
+                       c.Assert(err, check.IsNil)
+                       c.Assert(lnk.Properties, check.DeepEquals, tt.props)
+               } else {
+                       c.Assert(err, check.NotNil)
+               }
+       }
+}
+
+func (s *LinkSuite) TestLinkUpdateWithProperties(c *check.C) {
+       s.setUpVocabulary(c, "")
+       ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+
+       tests := []struct {
+               name    string
+               props   map[string]interface{}
+               success bool
+       }{
+               {"Invalid prop key", map[string]interface{}{"Priority": "IDVALIMPORTANCES1"}, false},
+               {"Invalid prop value", map[string]interface{}{"IDTAGIMPORTANCES": "high"}, false},
+               {"Valid prop key & value", map[string]interface{}{"IDTAGIMPORTANCES": "IDVALIMPORTANCES1"}, true},
+               {"Empty properties", map[string]interface{}{}, true},
+       }
+       for _, tt := range tests {
+               c.Log(c.TestName()+" ", tt.name)
+               lnk, err := s.localdb.LinkCreate(ctx, arvados.CreateOptions{
+                       Attrs: map[string]interface{}{
+                               "link_class": "star",
+                               "tail_uuid":  "zzzzz-j7d0g-publicfavorites",
+                               "head_uuid":  arvadostest.FooCollection,
+                       },
+               })
+               c.Assert(err, check.IsNil)
+               lnk, err = s.localdb.LinkUpdate(ctx, arvados.UpdateOptions{
+                       UUID:   lnk.UUID,
+                       Select: []string{"uuid", "properties"},
+                       Attrs: map[string]interface{}{
+                               "properties": tt.props,
+                       }})
+               if tt.success {
+                       c.Assert(err, check.IsNil)
+                       c.Assert(lnk.Properties, check.DeepEquals, tt.props)
+               } else {
+                       c.Assert(err, check.NotNil)
+               }
+       }
+}
index d04eccf689cb09dba9208ebb4a6e20fcb40f5783..02e06279f1168adca61999a543a9e82ad059e424 100644 (file)
@@ -314,6 +314,41 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.GroupUntrash(ctx, *opts.(*arvados.UntrashOptions))
                        },
                },
+               {
+                       arvados.EndpointLinkCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
                {
                        arvados.EndpointSpecimenCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
index 1acddfab71ec78d4487be4451678b0a921014eb1..25f47bc3bac4f801f2aa33b90e2ab935b0f651f9 100644 (file)
@@ -502,6 +502,41 @@ func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptio
        return resp, err
 }
 
+func (conn *Conn) LinkCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Link, error) {
+       ep := arvados.EndpointLinkCreate
+       var resp arvados.Link
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) LinkUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Link, error) {
+       ep := arvados.EndpointLinkUpdate
+       var resp arvados.Link
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) LinkGet(ctx context.Context, options arvados.GetOptions) (arvados.Link, error) {
+       ep := arvados.EndpointLinkGet
+       var resp arvados.Link
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) LinkList(ctx context.Context, options arvados.ListOptions) (arvados.LinkList, error) {
+       ep := arvados.EndpointLinkList
+       var resp arvados.LinkList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) LinkDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Link, error) {
+       ep := arvados.EndpointLinkDelete
+       var resp arvados.Link
+       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
index 41727beea811e8649045f7235584ed7415f80d97..0fdc13d1985d085c28db23615dd9ce1c673781cd 100644 (file)
@@ -63,6 +63,11 @@ var (
        EndpointGroupDelete                   = APIEndpoint{"DELETE", "arvados/v1/groups/{uuid}", ""}
        EndpointGroupTrash                    = APIEndpoint{"POST", "arvados/v1/groups/{uuid}/trash", ""}
        EndpointGroupUntrash                  = APIEndpoint{"POST", "arvados/v1/groups/{uuid}/untrash", ""}
+       EndpointLinkCreate                    = APIEndpoint{"POST", "arvados/v1/links", "link"}
+       EndpointLinkUpdate                    = APIEndpoint{"PATCH", "arvados/v1/links/{uuid}", "link"}
+       EndpointLinkGet                       = APIEndpoint{"GET", "arvados/v1/links/{uuid}", ""}
+       EndpointLinkList                      = APIEndpoint{"GET", "arvados/v1/links", ""}
+       EndpointLinkDelete                    = APIEndpoint{"DELETE", "arvados/v1/links/{uuid}", ""}
        EndpointUserActivate                  = APIEndpoint{"POST", "arvados/v1/users/{uuid}/activate", ""}
        EndpointUserCreate                    = APIEndpoint{"POST", "arvados/v1/users", "user"}
        EndpointUserCurrent                   = APIEndpoint{"GET", "arvados/v1/users/current", ""}
@@ -254,6 +259,11 @@ type API interface {
        GroupDelete(ctx context.Context, options DeleteOptions) (Group, error)
        GroupTrash(ctx context.Context, options DeleteOptions) (Group, error)
        GroupUntrash(ctx context.Context, options UntrashOptions) (Group, error)
+       LinkCreate(ctx context.Context, options CreateOptions) (Link, error)
+       LinkUpdate(ctx context.Context, options UpdateOptions) (Link, error)
+       LinkGet(ctx context.Context, options GetOptions) (Link, error)
+       LinkList(ctx context.Context, options ListOptions) (LinkList, error)
+       LinkDelete(ctx context.Context, options DeleteOptions) (Link, 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)
index f7d1f35a3c322953c702437ca5caecd40687bddd..7df6b84d60eb338fd833944940a2f966192960c2 100644 (file)
@@ -4,17 +4,25 @@
 
 package arvados
 
+import "time"
+
 // Link is an arvados#link record
 type Link struct {
-       UUID       string                 `json:"uuid,omiempty"`
-       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"`
-       Properties map[string]interface{} `json:"properties"`
+       UUID                 string                 `json:"uuid,omitempty"`
+       Etag                 string                 `json:"etag"`
+       Href                 string                 `json:"href"`
+       OwnerUUID            string                 `json:"owner_uuid"`
+       Name                 string                 `json:"name"`
+       LinkClass            string                 `json:"link_class"`
+       CreatedAt            time.Time              `json:"created_at"`
+       ModifiedAt           time.Time              `json:"modified_at"`
+       ModifiedByClientUUID string                 `json:"modified_by_client_uuid"`
+       ModifiedByUserUUID   string                 `json:"modified_by_user_uuid"`
+       HeadUUID             string                 `json:"head_uuid"`
+       HeadKind             string                 `json:"head_kind"`
+       TailUUID             string                 `json:"tail_uuid"`
+       TailKind             string                 `json:"tail_kind"`
+       Properties           map[string]interface{} `json:"properties"`
 }
 
 // LinkList is an arvados#linkList resource.
index 2cb35366c0f5fcc9984f1a41f0d5e6ea6e813ad3..0af477125b737a65f1fad46fce3009f5e27d1bcd 100644 (file)
@@ -169,6 +169,26 @@ func (as *APIStub) GroupUntrash(ctx context.Context, options arvados.UntrashOpti
        as.appendCall(ctx, as.GroupUntrash, options)
        return arvados.Group{}, as.Error
 }
+func (as *APIStub) LinkCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Link, error) {
+       as.appendCall(ctx, as.LinkCreate, options)
+       return arvados.Link{}, as.Error
+}
+func (as *APIStub) LinkUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Link, error) {
+       as.appendCall(ctx, as.LinkUpdate, options)
+       return arvados.Link{}, as.Error
+}
+func (as *APIStub) LinkGet(ctx context.Context, options arvados.GetOptions) (arvados.Link, error) {
+       as.appendCall(ctx, as.LinkGet, options)
+       return arvados.Link{}, as.Error
+}
+func (as *APIStub) LinkList(ctx context.Context, options arvados.ListOptions) (arvados.LinkList, error) {
+       as.appendCall(ctx, as.LinkList, options)
+       return arvados.LinkList{}, as.Error
+}
+func (as *APIStub) LinkDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Link, error) {
+       as.appendCall(ctx, as.LinkDelete, options)
+       return arvados.Link{}, as.Error
+}
 func (as *APIStub) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
        as.appendCall(ctx, as.SpecimenCreate, options)
        return arvados.Specimen{}, as.Error