Add noms root command for getting and setting root of entire db (#2992)

* Add `noms root` command for getting and setting root of entire db

* fix missing import

* rebase, review feedback

* review feedback

* moar review
This commit is contained in:
Aaron Boodman
2017-01-30 17:34:06 -08:00
committed by GitHub
parent 04eaf9d326
commit 0b596da7be
12 changed files with 209 additions and 11 deletions

View File

@@ -23,6 +23,7 @@ var commands = []*util.Command{
nomsLog,
nomsMerge,
nomsMigrate,
nomsRoot,
nomsServe,
nomsShow,
nomsSync,

112
cmd/noms/noms_root.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"fmt"
"os"
"strings"
"github.com/attic-labs/noms/cmd/util"
"github.com/attic-labs/noms/go/config"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/hash"
"github.com/attic-labs/noms/go/types"
flag "github.com/juju/gnuflag"
)
var nomsRoot = &util.Command{
Run: runRoot,
UsageLine: "root <db-spec>",
Short: "Get or set the current root hash of the entire database",
Long: "See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the database argument.",
Flags: setupRootFlags,
Nargs: 1,
}
var updateRoot = ""
func setupRootFlags() *flag.FlagSet {
flagSet := flag.NewFlagSet("root", flag.ExitOnError)
flagSet.StringVar(&updateRoot, "update", "", "Replaces the entire database with the one with the given hash")
return flagSet
}
func runRoot(args []string) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Not enough arguments")
return 0
}
cfg := config.NewResolver()
rt, err := cfg.GetRootTracker(args[0])
d.CheckErrorNoUsage(err)
currRoot := rt.Root()
if updateRoot == "" {
fmt.Println(currRoot)
return 0
}
if updateRoot[0] == '#' {
updateRoot = updateRoot[1:]
}
h, ok := hash.MaybeParse(updateRoot)
if !ok {
fmt.Fprintf(os.Stderr, "Invalid hash: %s\n", h.String())
return 1
}
db, err := cfg.GetDatabase(args[0])
d.CheckErrorNoUsage(err)
defer db.Close()
if !validate(db.ReadValue(h)) {
return 1
}
fmt.Println(`WARNING
This operation replaces the entire database with the instance having the given
hash. The old database becomes eligible for GC.
ANYTHING NOT SAVED WILL BE LOST
Continue?
`)
var input string
n, err := fmt.Scanln(&input)
d.CheckErrorNoUsage(err)
if n != 1 || strings.ToLower(input) != "y" {
return 0
}
ok = rt.UpdateRoot(h, currRoot)
if !ok {
fmt.Fprintln(os.Stderr, "Optimistic concurrency failure")
return 1
}
fmt.Printf("Success. Previous root was: %s\n", currRoot)
return 0
}
func validate(r types.Value) bool {
rootType := types.MakeMapType(types.StringType, types.MakeRefType(types.ValueType))
if !types.IsSubtype(rootType, r.Type()) {
fmt.Fprintf(os.Stderr, "Root of database must be %s, but you specified: %s\n", rootType.Describe(), r.Type().Describe())
return false
}
return r.(types.Map).Any(func(k, v types.Value) bool {
if !datas.IsRefOfCommitType(v.Type()) {
fmt.Fprintf(os.Stderr, "Invalid root map. Value for key '%s' is not a ref of commit.", string(k.(types.String)))
return false
}
return true
})
}

View File

@@ -0,0 +1,43 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"testing"
"github.com/attic-labs/noms/go/spec"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/clienttest"
"github.com/attic-labs/testify/suite"
)
func TestNomsRoot(t *testing.T) {
suite.Run(t, &nomsRootTestSuite{})
}
type nomsRootTestSuite struct {
clienttest.ClientTestSuite
}
func (s *nomsRootTestSuite) TestBasic() {
datasetName := "root-get"
dsSpec := spec.CreateValueSpecString("ldb", s.LdbDir, datasetName)
sp, err := spec.ForDataset(dsSpec)
s.NoError(err)
defer sp.Close()
ds := sp.GetDataset()
dbSpecStr := spec.CreateDatabaseSpecString("ldb", s.LdbDir)
ds, _ = ds.Database().CommitValue(ds, types.String("hello!"))
c1, _ := s.MustRun(main, []string{"root", dbSpecStr})
s.Equal("gt8mq6r7hvccp98s2vpeu9v9ct4rhloc\n", c1)
ds, _ = ds.Database().CommitValue(ds, types.String("goodbye"))
c2, _ := s.MustRun(main, []string{"root", dbSpecStr})
s.Equal("8tj5ctfhbka8fag417huneepg5ji283u\n", c2)
// TODO: Would be good to test successful --update too, but requires changes to MustRun to allow
// input because of prompt :(.
}

View File

@@ -104,6 +104,19 @@ func (r *Resolver) GetChunkStore(str string) (chunks.ChunkStore, error) {
return sp.NewChunkStore(), nil
}
// Resolve string to a RootTracker. Like ResolveDatabase, but returns a RootTracker instead
func (r *Resolver) GetRootTracker(str string) (chunks.RootTracker, error) {
sp, err := spec.ForDatabase(r.verbose(str, r.ResolveDbSpec(str)))
if err != nil {
return nil, err
}
var rt chunks.RootTracker = sp.NewChunkStore()
if rt == nil {
rt = datas.NewHTTPBatchStore(sp.Spec, "")
}
return rt, nil
}
// Resolve string to a dataset. If a config is present,
// - if no db prefix is present, assume the default db
// - if the db prefix is an alias, replace it

View File

@@ -94,8 +94,13 @@ type Database interface {
// Regardless, Datasets() is updated to match backing storage upon return.
FastForward(ds Dataset, newHeadRef types.Ref) (Dataset, error)
has(h hash.Hash) bool
// validatingBatchStore returns the BatchStore used to read and write
// groups of values to the database efficiently. This interface is a low-
// level detail of the database that should infrequently be needed by
// clients.
validatingBatchStore() types.BatchStore
has(h hash.Hash) bool
}
func NewDatabase(cs chunks.ChunkStore) Database {

View File

@@ -60,7 +60,7 @@ type RemoteDatabaseSuite struct {
func (suite *RemoteDatabaseSuite) SetupTest() {
suite.cs = chunks.NewTestStore()
suite.makeDb = func(cs chunks.ChunkStore) Database {
hbs := newHTTPBatchStoreForTest(cs)
hbs := NewHTTPBatchStoreForTest(cs)
return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(hbs), types.NewValueStore(hbs), hbs)}
}
suite.db = suite.makeDb(suite.cs)

View File

@@ -53,7 +53,7 @@ type httpBatchStore struct {
hints types.Hints
}
func newHTTPBatchStore(baseURL, auth string) *httpBatchStore {
func NewHTTPBatchStore(baseURL, auth string) *httpBatchStore {
u, err := url.Parse(baseURL)
d.PanicIfError(err)
if u.Scheme != "http" && u.Scheme != "https" {

View File

@@ -49,10 +49,10 @@ func (serv inlineServer) Do(req *http.Request) (resp *http.Response, err error)
func (suite *HTTPBatchStoreSuite) SetupTest() {
suite.cs = chunks.NewTestStore()
suite.store = newHTTPBatchStoreForTest(suite.cs)
suite.store = NewHTTPBatchStoreForTest(suite.cs)
}
func newHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore {
func NewHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore {
serv := inlineServer{httprouter.New()}
serv.POST(
constants.WriteValuePath,
@@ -84,7 +84,7 @@ func newHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore {
HandleRootGet(w, req, ps, cs)
},
)
hcs := newHTTPBatchStore("http://localhost:9000", "")
hcs := NewHTTPBatchStore("http://localhost:9000", "")
hcs.httpClient = serv
return hcs
}
@@ -102,7 +102,7 @@ func newAuthenticatingHTTPBatchStoreForTest(suite *HTTPBatchStoreSuite, hostUrl
HandleRootPost(w, req, ps, suite.cs)
},
)
hcs := newHTTPBatchStore(hostUrl, "")
hcs := NewHTTPBatchStore(hostUrl, "")
hcs.httpClient = serv
return hcs
}
@@ -116,7 +116,7 @@ func newBadVersionHTTPBatchStoreForTest(suite *HTTPBatchStoreSuite) *httpBatchSt
w.Header().Set(NomsVersionHeader, "BAD")
},
)
hcs := newHTTPBatchStore("http://localhost", "")
hcs := NewHTTPBatchStore("http://localhost", "")
hcs.httpClient = serv
return hcs
}
@@ -210,7 +210,7 @@ func (b *backpressureCS) PutMany(chnx []chunks.Chunk) chunks.BackpressureError {
func (suite *HTTPBatchStoreSuite) TestPutChunksBackpressure() {
bpcs := &backpressureCS{ChunkStore: suite.cs}
bs := newHTTPBatchStoreForTest(bpcs)
bs := NewHTTPBatchStoreForTest(bpcs)
defer bs.Close()
defer bpcs.Close()

View File

@@ -85,7 +85,7 @@ func (suite *RemoteToRemoteSuite) SetupTest() {
}
func makeRemoteDb(cs chunks.ChunkStore) Database {
hbs := newHTTPBatchStoreForTest(cs)
hbs := NewHTTPBatchStoreForTest(cs)
return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(hbs), types.NewValueStore(hbs), hbs)}
}

View File

@@ -15,7 +15,7 @@ type RemoteDatabaseClient struct {
}
func NewRemoteDatabase(baseURL, auth string) *RemoteDatabaseClient {
httpBS := newHTTPBatchStore(baseURL, auth)
httpBS := NewHTTPBatchStore(baseURL, auth)
return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(httpBS), types.NewValueStore(httpBS), httpBS)}
}

View File

@@ -247,6 +247,18 @@ func (m Map) Iter(cb mapIterCallback) {
})
}
// Any returns true if cb() return true for any of the items in the map.
func (m Map) Any(cb func(k, v Value) bool) (yep bool) {
m.Iter(func(k, v Value) bool {
if cb(k, v) {
yep = true
return true
}
return false
})
return
}
type mapIterAllCallback func(key, value Value)
func (m Map) IterAll(cb mapIterAllCallback) {

View File

@@ -808,6 +808,18 @@ func TestMapIter2(t *testing.T) {
doTest(getTestRefToValueOrderMap(2, NewTestValueStore()))
}
func TestMapAny(t *testing.T) {
assert := assert.New(t)
p := func(k, v Value) bool {
return k.Equals(String("foo")) && v.Equals(String("bar"))
}
assert.False(NewMap().Any(p))
assert.False(NewMap(String("foo"), String("baz")).Any(p))
assert.True(NewMap(String("foo"), String("bar")).Any(p))
}
func TestMapIterAll(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test in short mode.")