diff --git a/controller/resourceStore.go b/controller/resourceStore.go index 10409311..4529747e 100644 --- a/controller/resourceStore.go +++ b/controller/resourceStore.go @@ -18,22 +18,15 @@ package controller import ( "errors" + "reflect" + + log "github.com/Sirupsen/logrus" "github.com/coreos/etcd/client" "github.com/satori/go.uuid" "golang.org/x/net/context" - "reflect" ) type ( - resource interface { - key() string - } - - serializer interface { - serialize(r resource) ([]byte, error) - deserialize(buf []byte, r resource) error - } - resourceStore struct { *fileStore client.KeysAPI @@ -62,7 +55,7 @@ func getKey(r resource) (string, error) { if err != nil { return "", err } - rkey := r.key() + rkey := r.Key() return (typName + "/" + rkey), nil } @@ -139,6 +132,7 @@ func (rs *resourceStore) writeFile(parentKey string, contents []byte) (string, s return "", "", err } + parentKey = "file/" + parentKey resp, err := rs.KeysAPI.CreateInOrder(context.Background(), parentKey, uid, nil) if err != nil { _ = rs.fileStore.delete(uid) @@ -149,6 +143,7 @@ func (rs *resourceStore) writeFile(parentKey string, contents []byte) (string, s } func (rs *resourceStore) readFile(key string, uid *string) ([]byte, error) { + key = "file/" + key resp, err := rs.KeysAPI.Get(context.Background(), key, &client.GetOptions{Sort: true}) if err != nil { return nil, err @@ -168,7 +163,7 @@ func (rs *resourceStore) readFile(key string, uid *string) ([]byte, error) { } } if !found { - return nil, errors.New("Invalid UUID") + return nil, errors.New("Invalid UID " + *uid) } } @@ -177,15 +172,57 @@ func (rs *resourceStore) readFile(key string, uid *string) ([]byte, error) { } func (rs *resourceStore) deleteFile(key string, uid string) error { + key = "file/" + key + resp, err := rs.KeysAPI.Get(context.Background(), key, &client.GetOptions{Sort: true}) + if err != nil { + return err + } + + var node *client.Node + for _, u := range resp.Node.Nodes { + if u.Value == uid { + node = u + } + } + if node == nil { + log.WithFields(log.Fields{"key": key, "uid": uid}).Error("unreferenced file") + return errors.New("won't delete unreferenced file") + } + + err = rs.fileStore.delete(node.Value) + if err != nil { + return err + } + + _, err = rs.KeysAPI.Delete(context.Background(), node.Key, nil) + if err != nil { + return err + } + + if len(resp.Node.Nodes) == 1 { + _, err = rs.KeysAPI.Delete(context.Background(), key, &client.DeleteOptions{Dir: true}) + return err + } + return nil +} + +func (rs *resourceStore) deleteAllFiles(key string) error { + key = "file/" + key resp, err := rs.KeysAPI.Get(context.Background(), key, &client.GetOptions{Sort: true}) if err != nil { return err } for _, u := range resp.Node.Nodes { - if u.Value == uid { - err = rs.fileStore.delete(u.Value) + err = rs.fileStore.delete(u.Value) + if err != nil { + return err + } + + _, err = rs.KeysAPI.Delete(context.Background(), u.Key, nil) + if err != nil { return err } } - return errors.New("won't delete unreferenced file") + _, err = rs.KeysAPI.Delete(context.Background(), key, &client.DeleteOptions{Dir: true}) + return err } diff --git a/controller/resourceStore_test.go b/controller/resourceStore_test.go index cb2a1b1b..5ff312c3 100644 --- a/controller/resourceStore_test.go +++ b/controller/resourceStore_test.go @@ -17,12 +17,13 @@ limitations under the License. package controller import ( - // "github.com/coreos/etcd/client" - "golang.org/x/net/context" "io/ioutil" "log" "os" "testing" + + "github.com/coreos/etcd/client" + "golang.org/x/net/context" ) type TestResource struct { @@ -30,11 +31,11 @@ type TestResource struct { B int } -func (tr TestResource) key() string { +func (tr TestResource) Key() string { return tr.A } -func check(err error) { +func panicIf(err error) { if err != nil { log.Panicf("err: %v", err) } @@ -46,91 +47,101 @@ func assert(b bool, msg string) { } } -func TestResourceStore(t *testing.T) { +func getTestResourceStore() (*fileStore, client.KeysAPI, *resourceStore) { // make a tmp dir dir, err := ioutil.TempDir("", "testFileStore") - check(err) - defer os.RemoveAll(dir) - + panicIf(err) fs := makeFileStore(dir) // assume etcd is running, connect to it ks := getEtcdKeyAPI([]string{"http://localhost:2379"}) + s := JsonSerializer{} rs := makeResourceStore(fs, ks, s) + return fs, ks, rs +} + +func TestResourceStore(t *testing.T) { + fs, ks, rs := getTestResourceStore() + defer os.RemoveAll(fs.root) + + s := JsonSerializer{} + tr := TestResource{A: "hello", B: 1} // Delete the key first, in case of a panic'd previous test run; ignore errors - _ = rs.delete("TestResource", tr.key()) + _ = rs.delete("TestResource", tr.Key()) // Create - err = rs.create(tr) - check(err) - defer rs.delete("TestResource", tr.key()) + err := rs.create(tr) + panicIf(err) + defer ks.Delete(context.Background(), "/TestResource", &client.DeleteOptions{Dir: true}) + defer rs.delete("TestResource", tr.Key()) // Etcd key /TestResource/hello should exist _, err = ks.Get(context.Background(), "TestResource/hello", nil) - check(err) + panicIf(err) // Read tr1 := TestResource{} - err = rs.read(tr.key(), &tr1) - check(err) + err = rs.read(tr.Key(), &tr1) + panicIf(err) assert(tr1 == tr, "retrieved value must equal created value") // Update and Read tr.B += 1 err = rs.update(tr) - check(err) - err = rs.read(tr.key(), &tr1) - check(err) + panicIf(err) + err = rs.read(tr.Key(), &tr1) + panicIf(err) assert(tr1 == tr, "retrieved value must equal updated value") // Get list results, err := rs.getAll("TestResource") - check(err) + panicIf(err) res := make([]TestResource, 0, 0) for _, r := range results { tmp := TestResource{} err = s.deserialize([]byte(r), &tmp) - check(err) + panicIf(err) res = append(res, tmp) } assert(res[0] == tr, "value from retrieved list must equal updated value") // file tests - fileKey := "foo" + fileKey := "resourceStoreTest" fileContents1 := []byte("hello") fileContents2 := []byte("world") key, uid1, err := rs.writeFile(fileKey, fileContents1) - check(err) + panicIf(err) defer rs.deleteFile(fileKey, uid1) log.Printf("key = %v, uid = %v", key, uid1) // read latest contents, err := rs.readFile(fileKey, nil) - check(err) + panicIf(err) assert(string(contents) == string(fileContents1), "retrieved file contents must match written value") // update-- same key new contents _, uid2, err := rs.writeFile(fileKey, fileContents2) - check(err) + panicIf(err) defer rs.deleteFile(fileKey, uid2) // read latest contents, err = rs.readFile(fileKey, nil) - check(err) + panicIf(err) assert(string(contents) == string(fileContents2), "retrieved file contents must match updated value") // read by uid // 1 contents, err = rs.readFile(fileKey, &uid1) - check(err) + panicIf(err) assert(string(contents) == string(fileContents1), "retrieved file contents must match updated value") // 2 contents, err = rs.readFile(fileKey, &uid2) - check(err) + panicIf(err) assert(string(contents) == string(fileContents2), "retrieved file contents must match updated value") + } diff --git a/controller/types.go b/controller/types.go new file mode 100644 index 00000000..abc575ec --- /dev/null +++ b/controller/types.go @@ -0,0 +1,28 @@ +/* +Copyright 2016 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +type ( + resource interface { + Key() string + } + + serializer interface { + serialize(r resource) ([]byte, error) + deserialize(buf []byte, r resource) error + } +)