diff --git a/controller/jsonSerializer.go b/controller/jsonSerializer.go new file mode 100644 index 00000000..008ba608 --- /dev/null +++ b/controller/jsonSerializer.go @@ -0,0 +1,32 @@ +/* +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 + +import ( + "encoding/json" +) + +type JsonSerializer struct { +} + +func (j JsonSerializer) serialize(r resource) ([]byte, error) { + return json.Marshal(r) +} + +func (j JsonSerializer) deserialize(buf []byte, r resource) error { + return json.Unmarshal(buf, r) +} diff --git a/controller/resourceStore.go b/controller/resourceStore.go new file mode 100644 index 00000000..10409311 --- /dev/null +++ b/controller/resourceStore.go @@ -0,0 +1,191 @@ +/* +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 + +import ( + "errors" + "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 + serializer + } +) + +func makeResourceStore(fs *fileStore, ks client.KeysAPI, s serializer) *resourceStore { + return &resourceStore{fileStore: fs, KeysAPI: ks, serializer: s} +} + +func getTypeName(r resource) (string, error) { + typ := reflect.TypeOf(r) + if typ.Kind().String() == "ptr" { + typ = typ.Elem() + } + typName := typ.Name() + if len(typName) == 0 { + return "", errors.New("Failed to get type") + } + return typName, nil +} + +func getKey(r resource) (string, error) { + typName, err := getTypeName(r) + if err != nil { + return "", err + } + rkey := r.key() + return (typName + "/" + rkey), nil +} + +func (rs *resourceStore) create(r resource) error { + key, err := getKey(r) + if err != nil { + return err + } + + serialized, err := rs.serializer.serialize(r) + if err != nil { + return err + } + + _, err = rs.KeysAPI.Set(context.Background(), key, string(serialized), + &client.SetOptions{PrevExist: client.PrevNoExist}) + return err +} + +func (rs *resourceStore) read(rkey string, res resource) error { + typName, err := getTypeName(res) + if err != nil { + return err + } + key := typName + "/" + rkey + + resp, err := rs.KeysAPI.Get(context.Background(), key, nil) + if err != nil { + return err + } + return rs.serializer.deserialize([]byte(resp.Node.Value), res) +} + +func (rs *resourceStore) update(r resource) error { + key, err := getKey(r) + if err != nil { + return err + } + + serialized, err := rs.serializer.serialize(r) + if err != nil { + return err + } + + _, err = rs.KeysAPI.Set(context.Background(), key, string(serialized), + &client.SetOptions{PrevExist: client.PrevExist}) + return err +} + +func (rs *resourceStore) delete(typename, rkey string) error { + key := typename + "/" + rkey + _, err := rs.KeysAPI.Delete(context.Background(), key, nil) // ignore response + return err +} + +func (rs *resourceStore) getAll(key string) ([]string, error) { + resp, err := rs.KeysAPI.Get(context.Background(), key, &client.GetOptions{Recursive: true}) + if err != nil { + return nil, err + } + + res := make([]string, 0, len(resp.Node.Nodes)) + for _, n := range resp.Node.Nodes { + res = append(res, n.Value) + } + return res, nil +} + +func (rs *resourceStore) writeFile(parentKey string, contents []byte) (string, string, error) { + uid := uuid.NewV4().String() + + err := rs.fileStore.write(uid, contents) + if err != nil { + return "", "", err + } + + resp, err := rs.KeysAPI.CreateInOrder(context.Background(), parentKey, uid, nil) + if err != nil { + _ = rs.fileStore.delete(uid) + return "", "", err + } + + return resp.Node.Key, uid, nil +} + +func (rs *resourceStore) readFile(key string, uid *string) ([]byte, error) { + resp, err := rs.KeysAPI.Get(context.Background(), key, &client.GetOptions{Sort: true}) + if err != nil { + return nil, err + } + + if uid == nil { + // get latest + n := resp.Node.Nodes + uid = &n[len(n)-1].Value + } else { + // validate uid is in the list + found := false + for _, u := range resp.Node.Nodes { + if *uid == u.Value { + found = true + break + } + } + if !found { + return nil, errors.New("Invalid UUID") + } + } + + contents, err := rs.fileStore.read(*uid) + return contents, err +} + +func (rs *resourceStore) deleteFile(key string, uid string) error { + 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) + return err + } + } + return errors.New("won't delete unreferenced file") +} diff --git a/controller/resourceStore_test.go b/controller/resourceStore_test.go new file mode 100644 index 00000000..cb2a1b1b --- /dev/null +++ b/controller/resourceStore_test.go @@ -0,0 +1,136 @@ +/* +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 + +import ( + // "github.com/coreos/etcd/client" + "golang.org/x/net/context" + "io/ioutil" + "log" + "os" + "testing" +) + +type TestResource struct { + A string + B int +} + +func (tr TestResource) key() string { + return tr.A +} + +func check(err error) { + if err != nil { + log.Panicf("err: %v", err) + } +} + +func assert(b bool, msg string) { + if !b { + log.Panic("assertion failed: " + msg) + } +} + +func TestResourceStore(t *testing.T) { + // make a tmp dir + dir, err := ioutil.TempDir("", "testFileStore") + check(err) + defer os.RemoveAll(dir) + + fs := makeFileStore(dir) + + // assume etcd is running, connect to it + ks := getEtcdKeyAPI([]string{"http://localhost:2379"}) + s := JsonSerializer{} + rs := makeResourceStore(fs, ks, s) + + 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()) + + // Create + err = rs.create(tr) + check(err) + defer rs.delete("TestResource", tr.key()) + + // Etcd key /TestResource/hello should exist + _, err = ks.Get(context.Background(), "TestResource/hello", nil) + check(err) + + // Read + tr1 := TestResource{} + err = rs.read(tr.key(), &tr1) + check(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) + assert(tr1 == tr, "retrieved value must equal updated value") + + // Get list + results, err := rs.getAll("TestResource") + check(err) + res := make([]TestResource, 0, 0) + for _, r := range results { + tmp := TestResource{} + err = s.deserialize([]byte(r), &tmp) + check(err) + res = append(res, tmp) + } + assert(res[0] == tr, "value from retrieved list must equal updated value") + + // file tests + fileKey := "foo" + fileContents1 := []byte("hello") + fileContents2 := []byte("world") + key, uid1, err := rs.writeFile(fileKey, fileContents1) + check(err) + defer rs.deleteFile(fileKey, uid1) + log.Printf("key = %v, uid = %v", key, uid1) + + // read latest + contents, err := rs.readFile(fileKey, nil) + check(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) + defer rs.deleteFile(fileKey, uid2) + + // read latest + contents, err = rs.readFile(fileKey, nil) + check(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) + assert(string(contents) == string(fileContents1), "retrieved file contents must match updated value") + + // 2 + contents, err = rs.readFile(fileKey, &uid2) + check(err) + assert(string(contents) == string(fileContents2), "retrieved file contents must match updated value") +}