end
if tags
- props = @object.properties
- props[:tags] = tags
-
- if @object.update_attributes properties: props
+ if @object.update_attributes properties: tags
@saved_tags = true
render
else
end
def untrash
- arvados_api_client.api(self.class, "/#{self.uuid}/untrash", {})
+ arvados_api_client.api(self.class, "/#{self.uuid}/untrash", {"ensure_unique_name" => true})
end
end
<%
- tags = object.properties[:tags]
+ tags = object.properties
%>
<% if tags.andand.is_a?(Hash) %>
<% tags.each do |k, v| %>
assert_equal false, response.body.include?("existing tag 1")
assert_equal false, response.body.include?("value for existing tag 1")
- updated_properties = Collection.find(collection['uuid']).properties
- updated_tags = updated_properties[:tags]
+ updated_tags = Collection.find(collection['uuid']).properties
assert_equal true, updated_tags.keys.include?(:'new_tag1')
assert_equal new_tags['new_tag1'], updated_tags[:'new_tag1']
assert_equal true, updated_tags.keys.include?(:'new_tag2')
assert_equal new_tags['new_tag2'], updated_tags[:'new_tag2']
assert_equal false, updated_tags.keys.include?(:'existing tag 1')
assert_equal false, updated_tags.keys.include?(:'existing tag 2')
- assert_equal true, updated_properties.keys.include?(:'some other property')
- assert_equal 'value for the other property', updated_properties[:'some other property']
end
end
--- /dev/null
+require 'test_helper'
+
+class TrashItemsControllerTest < ActionController::TestCase
+ test "untrash collection with same name as another collection" do
+ collection = api_fixture('collections')['trashed_collection_to_test_name_conflict_on_untrash']
+ items = [collection['uuid']]
+ post :untrash_items, {
+ selection: items,
+ format: :js
+ }, session_for(:active)
+
+ assert_response :success
+ end
+end
assert(page.has_text?(foo_collection['uuid']), "Collection page did not include foo file")
assert(page.has_text?(bar_collection['uuid']), "Collection page did not include bar file")
- within('tr', text: foo_collection['uuid']) do
+ within "tr[data-object-uuid=\"#{foo_collection['uuid']}\"]" do
find('input[type=checkbox]').click
end
- within('tr', text: bar_collection['uuid']) do
+ within "tr[data-object-uuid=\"#{bar_collection['uuid']}\"]" do
find('input[type=checkbox]').click
end
As listed above the attributes that are used to manage a collection lifecycle are it's *is_trashed*, *trash_at*, and *delete_at*. The table below lists the values of these attributes and how they influence the state of a collection and it's accessibility.
table(table table-bordered table-condensed).
-|_. collection state|_. is_trashed|_. trash_at|_. delete_at|_. get|_. index|_. index?include_trash=true|_. can be modified|
+|_. collection state|_. is_trashed|_. trash_at|_. delete_at|_. get|_. list|_. list?include_trash=true|_. can be modified|
|persisted collection|false |null |null |yes |yes |yes |yes |
|expiring collection|false |future |future |yes |yes |yes |yes |
|trashed collection|true |past |future |no |no |yes |only is_trashed, trash_at and delete_at attribtues|
h3. Un-trashing a collection using arv command line tool
-You can list the trashed collections using the index command.
+You can list the trashed collections using the list command.
<pre>
-arv collection index --include-trash=true --filters '[["is_trashed", "=", "true"]]'
+arv collection list --include-trash=true --filters '[["is_trashed", "=", "true"]]'
</pre>
You can then untrash a particular collection using arv using it's uuid.
AdminToken = "4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h"
AnonymousToken = "4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi"
DataManagerToken = "320mkve8qkswstz7ff61glpk3mhgghmg67wmic7elw4z41pke1"
+ ManagementToken = "jg3ajndnq63sywcd50gbs5dskdc9ckkysb0nsqmfz08nwf17nl"
ActiveUserUUID = "zzzzz-tpzed-xurymjxw79nv3jz"
SpectatorUserUUID = "zzzzz-tpzed-l1s2piq4t4mps8r"
UserAgreementCollection = "zzzzz-4zz18-uukreo9rbgwsujr" // user_agreement_in_anonymously_accessible_project
def untrash
if @object.is_trashed
- @object.update_attributes!(trash_at: nil)
+ @object.trash_at = nil
+
+ if params[:ensure_unique_name]
+ @object.save_with_unique_name!
+ else
+ @object.save!
+ end
else
raise InvalidStateTransitionError
end
"https://api.curoverse.com/auth/arvados.readonly"
]
},
- list: {
- id: "arvados.#{k.to_s.underscore.pluralize}.list",
+ index: {
+ id: "arvados.#{k.to_s.underscore.pluralize}.index",
path: k.to_s.underscore.pluralize,
httpMethod: "GET",
description:
- %|List #{k.to_s.pluralize}.
+ %|Index #{k.to_s.pluralize}.
- The <code>list</code> method returns a
+ The <code>index</code> method returns a
<a href="/api/resources.html">resource list</a> of
matching #{k.to_s.pluralize}. For example:
}
</pre>|,
parameters: {
- limit: {
- type: "integer",
- description: "Maximum number of #{k.to_s.underscore.pluralize} to return.",
- default: "100",
- format: "int32",
- minimum: "0",
- location: "query",
- },
- offset: {
- type: "integer",
- description: "Number of #{k.to_s.underscore.pluralize} to skip before first returned record.",
- default: "0",
- format: "int32",
- minimum: "0",
- location: "query",
- },
- filters: {
- type: "array",
- description: "Conditions for filtering #{k.to_s.underscore.pluralize}.",
- location: "query"
- },
- where: {
- type: "object",
- description: "Conditions for filtering #{k.to_s.underscore.pluralize}. (Deprecated. Use filters instead.)",
- location: "query"
- },
- order: {
- type: "string",
- description: "Order in which to return matching #{k.to_s.underscore.pluralize}.",
- location: "query"
- },
- select: {
- type: "array",
- description: "Select which fields to return.",
- location: "query"
- },
- distinct: {
- type: "boolean",
- description: "Return each distinct object.",
- location: "query"
- },
- count: {
- type: "string",
- description: "Type of count to return in items_available ('none' or 'exact').",
- default: "exact",
- location: "query"
- }
},
response: {
"$ref" => "#{k.to_s}List"
end
end
d_methods[action.to_sym] = method
+
+ if action == 'index'
+ list_method = method.dup
+ list_method[:id].sub!('index', 'list')
+ list_method[:description].sub!('Index', 'List')
+ list_method[:description].sub!('index', 'list')
+ d_methods[:list] = list_method
+ end
end
end
end
uuid: zzzzz-4zz18-znfnqtbbv4spc3w
portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
owner_uuid: zzzzz-tpzed-000000000000000
- created_at: 2014-02-03T17:22:54Z
+ created_at: 2015-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
+ modified_at: 2015-02-03T17:22:54Z
+ updated_at: 2015-02-03T17:22:54Z
manifest_text: ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo\n"
name: foo_file
uuid: zzzzz-4zz18-ehbhgtheo8909or
portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
owner_uuid: zzzzz-tpzed-000000000000000
- created_at: 2014-02-03T17:22:54Z
+ created_at: 2015-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
+ modified_at: 2015-02-03T17:22:54Z
+ updated_at: 2015-02-03T17:22:54Z
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
name: bar_file
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
name: collection with tags
properties:
- tags:
- existing tag 1: value for existing tag 1
- existing tag 2: value for existing tag 2
- some other property: value for the other property
+ existing tag 1: value for existing tag 1
+ existing tag 2: value for existing tag 2
+
+trashed_collection_to_test_name_conflict_on_untrash:
+ uuid: zzzzz-4zz18-trashedcolnamec
+ portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ 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: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n"
+ name: same name for trashed and persisted collections
+ is_trashed: true
+ trash_at: 2001-01-01T00:00:00Z
+ delete_at: 2038-01-01T00:00:00Z
+
+same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
+ uuid: zzzzz-4zz18-namesameastrash
+ portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ 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: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n"
+ name: same name for trashed and persisted collections
# Test Helper trims the rest of the file
end
end
end
+
+ test 'untrash collection with same name as another with no ensure unique name' do
+ authorize_with :active
+ post :untrash, {
+ id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
+ }
+ assert_response 422
+ end
+
+ test 'untrash collection with same name as another with ensure unique name' do
+ authorize_with :active
+ post :untrash, {
+ id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
+ ensure_unique_name: true
+ }
+ assert_response 200
+ assert_equal false, json_response['is_trashed']
+ assert_nil json_response['trash_at']
+ assert_nil json_response['delete_at']
+ assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
+ end
end
PingTimeout arvados.Duration
ClientEventQueue int
ServerEventQueue int
+
+ ManagementToken string
}
func defaultConfig() wsConfig {
type eventSource interface {
NewSink() eventSink
DB() *sql.DB
+ DBHealth() error
}
type event struct {
return ps.db
}
+func (ps *pgEventSource) DBHealth() error {
+ ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
+ var i int
+ return ps.db.QueryRowContext(ctx, "SELECT 1").Scan(&i)
+}
+
func (ps *pgEventSource) DebugStatus() interface{} {
ps.mtx.Lock()
defer ps.mtx.Unlock()
"QueueDelay": stats.Duration(ps.lastQDelay),
"Sinks": len(ps.sinks),
"SinksBlocked": blocked,
+ "DBStats": ps.db.Stats(),
}
}
case <-time.After(10 * time.Second):
c.Fatal("timed out")
}
+
+ c.Check(pges.DBHealth(), check.IsNil)
}
rtr.mux = http.NewServeMux()
rtr.mux.Handle("/websocket", rtr.makeServer(newSessionV0))
rtr.mux.Handle("/arvados/v1/events.ws", rtr.makeServer(newSessionV1))
- rtr.mux.HandleFunc("/debug.json", jsonHandler(rtr.DebugStatus))
- rtr.mux.HandleFunc("/status.json", jsonHandler(rtr.Status))
+ rtr.mux.Handle("/debug.json", rtr.jsonHandler(rtr.DebugStatus))
+ rtr.mux.Handle("/status.json", rtr.jsonHandler(rtr.Status))
+
+ health := http.NewServeMux()
+ rtr.mux.Handle("/_health/", rtr.mgmtAuth(health))
+ health.Handle("/_health/ping", rtr.jsonHandler(rtr.HealthFunc(func() error { return nil })))
+ health.Handle("/_health/db", rtr.jsonHandler(rtr.HealthFunc(rtr.eventSource.DBHealth)))
}
func (rtr *router) makeServer(newSession sessionFactory) *websocket.Server {
return s
}
+var pingResponseOK = map[string]string{"health": "OK"}
+
+func (rtr *router) HealthFunc(f func() error) func() interface{} {
+ return func() interface{} {
+ err := f()
+ if err == nil {
+ return pingResponseOK
+ }
+ return map[string]string{
+ "health": "ERROR",
+ "error": err.Error(),
+ }
+ }
+}
+
func (rtr *router) Status() interface{} {
return map[string]interface{}{
"Clients": atomic.LoadInt64(&rtr.status.ReqsActive),
rtr.mux.ServeHTTP(resp, req)
}
-func jsonHandler(fn func() interface{}) http.HandlerFunc {
- return func(resp http.ResponseWriter, req *http.Request) {
- logger := logger(req.Context())
- resp.Header().Set("Content-Type", "application/json")
- enc := json.NewEncoder(resp)
+func (rtr *router) mgmtAuth(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if rtr.Config.ManagementToken == "" {
+ http.Error(w, "disabled", http.StatusNotFound)
+ } else if ah := r.Header.Get("Authorization"); ah == "" {
+ http.Error(w, "authorization required", http.StatusUnauthorized)
+ } else if ah != "Bearer "+rtr.Config.ManagementToken {
+ http.Error(w, "authorization error", http.StatusForbidden)
+ } else {
+ h.ServeHTTP(w, r)
+ }
+ })
+}
+
+func (rtr *router) jsonHandler(fn func() interface{}) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ logger := logger(r.Context())
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
err := enc.Encode(fn())
if err != nil {
msg := "encode failed"
logger.WithError(err).Error(msg)
- http.Error(resp, msg, http.StatusInternalServerError)
+ http.Error(w, msg, http.StatusInternalServerError)
}
- }
+ })
}
package main
import (
+ "io/ioutil"
+ "net/http"
"sync"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
check "gopkg.in/check.v1"
)
var _ = check.Suite(&serverSuite{})
type serverSuite struct {
+ cfg *wsConfig
+ srv *server
+ wg sync.WaitGroup
}
-func testConfig() *wsConfig {
+func (s *serverSuite) SetUpTest(c *check.C) {
+ s.cfg = s.testConfig()
+ s.srv = &server{wsConfig: s.cfg}
+}
+
+func (*serverSuite) testConfig() *wsConfig {
cfg := defaultConfig()
cfg.Client = *(arvados.NewClientFromEnv())
cfg.Postgres = testDBConfig()
cfg.Listen = ":"
+ cfg.ManagementToken = arvadostest.ManagementToken
return &cfg
}
// TestBadDB ensures Run() returns an error (instead of panicking or
// deadlocking) if it can't connect to the database server at startup.
func (s *serverSuite) TestBadDB(c *check.C) {
- cfg := testConfig()
- cfg.Postgres["password"] = "1234"
- srv := &server{wsConfig: cfg}
+ s.cfg.Postgres["password"] = "1234"
var wg sync.WaitGroup
wg.Add(1)
go func() {
- err := srv.Run()
+ err := s.srv.Run()
c.Check(err, check.NotNil)
wg.Done()
}()
wg.Add(1)
go func() {
- srv.WaitReady()
+ s.srv.WaitReady()
wg.Done()
}()
}
}
-func newTestServer() *server {
- srv := &server{wsConfig: testConfig()}
- go srv.Run()
- srv.WaitReady()
- return srv
+func (s *serverSuite) TestHealth(c *check.C) {
+ go s.srv.Run()
+ defer s.srv.Close()
+ s.srv.WaitReady()
+ for _, token := range []string{"", "foo", s.cfg.ManagementToken} {
+ req, err := http.NewRequest("GET", "http://"+s.srv.listener.Addr().String()+"/_health/ping", nil)
+ c.Assert(err, check.IsNil)
+ if token != "" {
+ req.Header.Add("Authorization", "Bearer "+token)
+ }
+ resp, err := http.DefaultClient.Do(req)
+ c.Check(err, check.IsNil)
+ if token == s.cfg.ManagementToken {
+ c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+ buf, err := ioutil.ReadAll(resp.Body)
+ c.Check(err, check.IsNil)
+ c.Check(string(buf), check.Equals, `{"health":"OK"}`+"\n")
+ } else {
+ c.Check(resp.StatusCode, check.Not(check.Equals), http.StatusOK)
+ }
+ }
+}
+
+func (s *serverSuite) TestHealthDisabled(c *check.C) {
+ s.cfg.ManagementToken = ""
+
+ go s.srv.Run()
+ defer s.srv.Close()
+ s.srv.WaitReady()
+
+ for _, token := range []string{"", "foo", arvadostest.ManagementToken} {
+ req, err := http.NewRequest("GET", "http://"+s.srv.listener.Addr().String()+"/_health/ping", nil)
+ c.Assert(err, check.IsNil)
+ req.Header.Add("Authorization", "Bearer "+token)
+ resp, err := http.DefaultClient.Do(req)
+ c.Check(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
+ }
}
sess.log.WithError(err).Error("db.Query failed")
return
}
+ defer rows.Close()
for rows.Next() {
var id uint64
err := rows.Scan(&id)
var _ = check.Suite(&v0Suite{})
type v0Suite struct {
- token string
- toDelete []string
+ serverSuite serverSuite
+ token string
+ toDelete []string
}
func (s *v0Suite) SetUpTest(c *check.C) {
+ s.serverSuite.SetUpTest(c)
s.token = arvadostest.ActiveToken
}
}
func (s *v0Suite) testClient() (*server, *websocket.Conn, *json.Decoder, *json.Encoder) {
- srv := newTestServer()
+ go s.serverSuite.srv.Run()
+ s.serverSuite.srv.WaitReady()
+ srv := s.serverSuite.srv
conn, err := websocket.Dial("ws://"+srv.listener.Addr().String()+"/websocket?api_token="+s.token, "", "http://"+srv.listener.Addr().String())
if err != nil {
panic(err)