Merge pull request #5 from platform9/controller

Controller
This commit is contained in:
Soam Vasani
2016-09-20 00:58:13 -07:00
committed by GitHub
16 changed files with 1573 additions and 14 deletions
+102
View File
@@ -0,0 +1,102 @@
/*
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 (
"fmt"
"net/http"
"os"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/platform9/fission"
)
type API struct {
FunctionStore
HTTPTriggerStore
EnvironmentStore
}
func (api *API) respondWithSuccess(w http.ResponseWriter, resp []byte) {
_, err := w.Write(resp)
if err != nil {
// this will probably fail too, but try anyway
api.respondWithError(w, err)
}
}
func (api *API) respondWithError(w http.ResponseWriter, err error) {
var code int
var msg string
fe, ok := err.(fission.Error)
if ok {
msg = fe.Message
switch fe.Code {
case fission.ErrorNotFound:
code = 404
case fission.ErrorInvalidArgument:
code = 400
case fission.ErrorNoSpace:
code = 500
case fission.ErrorNotAuthorized:
code = 403
default:
code = 500
}
} else {
code = 500
msg = err.Error()
}
log.Printf("Error: %v: %v", code, msg)
http.Error(w, msg, code)
}
func (api *API) HomeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Fission API")
}
func (api *API) serve(port int) {
r := mux.NewRouter()
r.HandleFunc("/", api.HomeHandler)
r.HandleFunc("/functions", api.FunctionApiList).Methods("GET")
r.HandleFunc("/functions", api.FunctionApiCreate).Methods("POST")
r.HandleFunc("/functions/{function}", api.FunctionApiGet).Methods("GET")
r.HandleFunc("/functions/{function}", api.FunctionApiUpdate).Methods("PUT")
r.HandleFunc("/functions/{function}", api.FunctionApiDelete).Methods("DELETE")
// r.HandleFunc("/triggers/http", api.HTTPTriggerApiList).Methods("GET")
// r.HandleFunc("/triggers/http", api.HTTPTriggerApiCreate).Methods("POST")
// r.HandleFunc("/triggers/http/{httpTrigger}", api.HTTPTriggerApiGet).Methods("GET")
// r.HandleFunc("/triggers/http/{httpTrigger}", api.HTTPTriggerApiUpdate).Methods("PUT")
// r.HandleFunc("/triggers/http/{httpTrigger}", api.HTTPTriggerApiDelete).Methods("DELETE")
// r.HandleFunc("/environments", api.EnvironmentApiList).Methods("GET")
// r.HandleFunc("/environments", api.EnvironmentApiCreate).Methods("POST")
// r.HandleFunc("/environments/{environment}", api.EnvironmentApiGet).Methods("GET")
// r.HandleFunc("/environments/{environment}", api.EnvironmentApiUpdate).Methods("PUT")
// r.HandleFunc("/environments/{environment}", api.EnvironmentApiDelete).Methods("DELETE")
address := fmt.Sprintf(":%v", port)
log.WithFields(log.Fields{"port": port}).Info("Server started")
log.Fatal(http.ListenAndServe(address, handlers.LoggingHandler(os.Stdout, r)))
}
+124
View File
@@ -0,0 +1,124 @@
/*
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 (
"io/ioutil"
"net/http"
"testing"
"time"
log "github.com/Sirupsen/logrus"
etcdClient "github.com/coreos/etcd/client"
"golang.org/x/net/context"
"github.com/platform9/fission"
"github.com/platform9/fission/controller/client"
)
func TestFunctionApi(t *testing.T) {
log.SetFormatter(&log.TextFormatter{DisableColors: true})
_, ks, rs := getTestResourceStore()
fs := &FunctionStore{resourceStore: *rs}
hts := &HTTPTriggerStore{resourceStore: *rs}
es := &EnvironmentStore{resourceStore: *rs}
api := &API{
FunctionStore: *fs,
HTTPTriggerStore: *hts,
EnvironmentStore: *es,
}
testFunc := &fission.Function{
Metadata: fission.Metadata{
Name: "foo",
Uid: "",
},
Environment: fission.Metadata{
Name: "nodejs",
Uid: "xxx",
},
Code: "code1",
}
go api.serve(8888)
time.Sleep(500 * time.Millisecond)
resp, err := http.Get("http://localhost:8888/")
panicIf(err)
_, err = ioutil.ReadAll(resp.Body)
panicIf(err)
client := client.New("http://localhost:8888")
_, err = ks.Delete(context.Background(), "Function", &etcdClient.DeleteOptions{Recursive: true})
if err != nil {
log.Printf("failed to delete: %v", err)
}
m, err := client.FunctionCreate(testFunc)
panicIf(err)
uid1 := m.Uid
log.Printf("Created function %v: %v", m.Name, m.Uid)
testFunc.Code = "code2"
m, err = client.FunctionUpdate(testFunc)
panicIf(err)
uid2 := m.Uid
log.Printf("Updated function %v: %v", m.Name, m.Uid)
m.Uid = uid1
testFunc.Code = "code1"
f, err := client.FunctionGet(m)
panicIf(err)
testFunc.Metadata.Uid = m.Uid
log.Printf("f = %#v", f)
log.Printf("testFunc = %#v", testFunc)
assert(*f == *testFunc, "first version should match when read by uid")
m.Uid = uid2
testFunc.Metadata.Uid = m.Uid
testFunc.Code = "code2"
f, err = client.FunctionGet(m)
panicIf(err)
assert(*f == *testFunc, "second version should match when read by uid")
m.Uid = ""
testFunc.Metadata.Uid = uid2
testFunc.Code = "code2"
f, err = client.FunctionGet(m)
panicIf(err)
assert(*f == *testFunc, "second version should match when read as latest")
testFunc.Metadata.Name = "bar"
m, err = client.FunctionCreate(testFunc)
panicIf(err)
funcs, err := client.FunctionList()
panicIf(err)
assert(len(funcs) == 2,
"created two functions, but didn't find them")
err = client.FunctionDelete(&fission.Metadata{Name: "foo"})
panicIf(err)
err = client.FunctionDelete(&fission.Metadata{Name: "bar"})
panicIf(err)
}
+214
View File
@@ -0,0 +1,214 @@
/*
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 client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
log "github.com/Sirupsen/logrus"
"github.com/platform9/fission"
)
type (
Client struct {
Url string
}
)
func New(serverUrl string) *Client {
return &Client{Url: serverUrl}
}
func (c *Client) delete(relativeUrl string) error {
req, err := http.NewRequest("DELETE", c.url(relativeUrl), nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.New("Delete failed")
} else {
return errors.New("Delete failed: " + string(body))
}
}
return nil
}
func (c *Client) put(relativeUrl string, contentType string, body []byte) (*http.Response, error) {
req, err := http.NewRequest("PUT", c.url(relativeUrl), bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-type", contentType)
return http.DefaultClient.Do(req)
}
func (c *Client) url(relativeUrl string) string {
return c.Url + "/" + relativeUrl
}
func (c *Client) FunctionCreate(f *fission.Function) (*fission.Metadata, error) {
reqbody, err := json.Marshal(f)
if err != nil {
return nil, err
}
resp, err := http.Post(c.url("functions"), "application/json", bytes.NewReader(reqbody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
log.WithFields(log.Fields{
"function": f.Metadata.Name,
"status": resp.StatusCode,
}).Error("Failed to create function")
return nil, errors.New("failed to create function")
}
var m fission.Metadata
err = json.Unmarshal(body, &m)
if err != nil {
return nil, err
}
return &m, nil
}
func (c *Client) FunctionGet(m *fission.Metadata) (*fission.Function, error) {
relativeUrl := fmt.Sprintf("functions/%v", m.Name)
if len(m.Uid) > 0 {
relativeUrl += fmt.Sprintf("?uid=%v", m.Uid)
}
resp, err := http.Get(c.url(relativeUrl))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var f fission.Function
err = json.Unmarshal(body, &f)
if err != nil {
return nil, err
}
return &f, nil
}
func (c *Client) FunctionUpdate(f *fission.Function) (*fission.Metadata, error) {
reqbody, err := json.Marshal(f)
if err != nil {
return nil, err
}
relativeUrl := fmt.Sprintf("functions/%v", f.Metadata.Name)
resp, err := c.put(relativeUrl, "application/json", reqbody)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var m fission.Metadata
err = json.Unmarshal(body, &m)
if err != nil {
return nil, err
}
return &m, nil
}
func (c *Client) FunctionDelete(m *fission.Metadata) error {
relativeUrl := fmt.Sprintf("functions/%v", m.Name)
if len(m.Uid) > 0 {
relativeUrl += fmt.Sprintf("?uid=%v", m.Uid)
}
err := c.delete(relativeUrl)
return err
}
func (c *Client) FunctionList() ([]fission.Function, error) {
resp, err := http.Get(c.url("functions"))
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
funcs := make([]fission.Function, 0)
err = json.Unmarshal(body, &funcs)
if err != nil {
return nil, err
}
return funcs, nil
}
// func (c *Client) HTTPTriggerCreate(f *fission.Function) (string, error) {
// }
// func (c *Client) HTTPTriggerGet(m *fission.Metadata) (*fission.HTTPTrigger, error) {
// }
// func (c *Client) HTTPTriggerUpdate(f *fission.HTTPTrigger) (string, error) {
// }
// func (c *Client) HTTPTriggerDelete(m *fission.Metadata) error {
// }
// func (c *Client) HTTPTriggerList() ([]fission.HTTPTrigger, error) {
// }
// func (c *Client) EnvironmentCreate(f *fission.Environment) (string, error) {
// }
// func (c *Client) EnvironmentGet(m *fission.Metadata) (*fission.Environment, error) {
// }
// func (c *Client) EnvironmentUpdate(f *fission.Environment) (string, error) {
// }
// func (c *Client) EnvironmentDelete(m *fission.Metadata) error {
// }
// func (c *Client) EnvironmentList() ([]fission.Environment, error) {
// }
+79
View File
@@ -0,0 +1,79 @@
/*
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/satori/go.uuid"
"github.com/platform9/fission"
)
type EnvironmentStore struct {
resourceStore
}
func (es *EnvironmentStore) create(e *fission.Environment) error {
e.Metadata.Uid = uuid.NewV4().String()
return es.resourceStore.create(e)
}
func (es *EnvironmentStore) read(m fission.Metadata) (*fission.Environment, error) {
var e fission.Environment
err := es.resourceStore.read(m.Name, &e)
if err != nil {
return nil, err
}
return &e, nil
}
func (es *EnvironmentStore) update(e *fission.Environment) error {
e.Metadata.Uid = uuid.NewV4().String()
return es.resourceStore.update(e)
}
func (es *EnvironmentStore) delete(m fission.Metadata) error {
typeName, err := getTypeName(fission.Environment{})
if err != nil {
return err
}
return es.resourceStore.delete(typeName, m.Name)
}
func (es *EnvironmentStore) list() ([]fission.Environment, error) {
typeName, err := getTypeName(fission.Environment{})
if err != nil {
return nil, err
}
bufs, err := es.resourceStore.getAll(typeName)
if err != nil {
return nil, err
}
triggers := make([]fission.Environment, 0, len(bufs))
js := JsonSerializer{}
for _, buf := range bufs {
var e fission.Environment
err = js.deserialize([]byte(buf), &e)
if err != nil {
return nil, err
}
triggers = append(triggers, e)
}
return triggers, nil
}
+115
View File
@@ -0,0 +1,115 @@
/*
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 (
"io/ioutil"
"os"
"path"
log "github.com/Sirupsen/logrus"
)
type requestType int
const (
READ requestType = iota
WRITE
DELETE
)
type (
fileStore struct {
root string // abs path of root of filestore
requestChannel chan fileStoreRequest
}
fileStoreRequest struct {
requestType
fileName string // relative path
fileContents []byte
responseChannel chan fileStoreResponse
}
fileStoreResponse struct {
error
fileContents []byte
}
)
func makeFileStore(path string) *fileStore {
fileStore := &fileStore{
root: path,
requestChannel: make(chan fileStoreRequest),
}
go fileStore.fileStoreService()
return fileStore
}
func (fs *fileStore) fileStoreService() {
for {
req := <-fs.requestChannel
response := &fileStoreResponse{}
log.WithFields(log.Fields{"file": req.fileName, "type": req.requestType}).Debug("fileStore request")
switch req.requestType {
case READ:
response.fileContents, response.error = ioutil.ReadFile(path.Join(fs.root, req.fileName))
case WRITE:
response.error = ioutil.WriteFile(path.Join(fs.root, req.fileName), req.fileContents, 0600)
case DELETE:
response.error = os.Remove(path.Join(fs.root, req.fileName))
default:
log.Panic("bad request")
}
req.responseChannel <- *response
}
}
func (fs *fileStore) read(fileName string) ([]byte, error) {
req := fileStoreRequest{
requestType: READ,
fileName: fileName,
responseChannel: make(chan fileStoreResponse),
}
fs.requestChannel <- req
response := <-req.responseChannel
return response.fileContents, response.error
}
func (fs *fileStore) write(fileName string, contents []byte) error {
req := fileStoreRequest{
requestType: WRITE,
fileName: fileName,
fileContents: contents,
responseChannel: make(chan fileStoreResponse),
}
fs.requestChannel <- req
response := <-req.responseChannel
return response.error
}
func (fs *fileStore) delete(fileName string) error {
req := fileStoreRequest{
requestType: DELETE,
fileName: fileName,
responseChannel: make(chan fileStoreResponse),
}
fs.requestChannel <- req
response := <-req.responseChannel
return response.error
}
+63
View File
@@ -0,0 +1,63 @@
/*
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 (
"io/ioutil"
"log"
"os"
"testing"
)
func TestFileStore(t *testing.T) {
// tmp dir
dir, err := ioutil.TempDir("", "testFileStore")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer os.RemoveAll(dir)
log.Printf("temp dir at %v", dir)
// file store
fs := makeFileStore(dir)
_, err = fs.read("nonexistent")
if err == nil {
t.Fatalf("expected an error")
}
path := "fileStoreTest"
contents := []byte("bar")
err = fs.write(path, contents)
if err != nil {
t.Fatalf("error: %v", err)
}
observedContents, err := fs.read(path)
if err != nil {
t.Fatalf("error: %v", err)
}
if string(observedContents) != string(contents) {
t.Fatalf("contents don't match")
}
err = fs.delete(path)
if err != nil {
t.Fatalf("error: %v", err)
}
}
+151
View File
@@ -0,0 +1,151 @@
/*
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 (
"io/ioutil"
"net/http"
"encoding/json"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
"github.com/platform9/fission"
)
func (api *API) FunctionApiList(w http.ResponseWriter, r *http.Request) {
funcs, err := api.FunctionStore.List()
if err != nil {
api.respondWithError(w, err)
return
}
resp, err := json.Marshal(funcs)
if err != nil {
api.respondWithError(w, err)
return
}
api.respondWithSuccess(w, resp)
}
func (api *API) FunctionApiCreate(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
api.respondWithError(w, err)
}
var f fission.Function
err = json.Unmarshal(body, &f)
if err != nil {
api.respondWithError(w, err)
return
}
uid, err := api.FunctionStore.Create(&f)
if err != nil {
api.respondWithError(w, err)
return
}
m := &fission.Metadata{Name: f.Name, Uid: uid}
resp, err := json.Marshal(m)
if err != nil {
api.respondWithError(w, err)
return
}
api.respondWithSuccess(w, resp)
}
func (api *API) FunctionApiGet(w http.ResponseWriter, r *http.Request) {
var m fission.Metadata
vars := mux.Vars(r)
m.Name = vars["function"]
m.Uid = r.FormValue("uid") // empty if uid is absent
f, err := api.FunctionStore.Get(&m)
if err != nil {
api.respondWithError(w, err)
return
}
resp, err := json.Marshal(f)
if err != nil {
api.respondWithError(w, err)
return
}
api.respondWithSuccess(w, resp)
}
func (api *API) FunctionApiUpdate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funcName := vars["function"]
body, err := ioutil.ReadAll(r.Body)
if err != nil {
api.respondWithError(w, err)
}
var f fission.Function
err = json.Unmarshal(body, &f)
if err != nil {
api.respondWithError(w, err)
return
}
if funcName != f.Metadata.Name {
err = fission.MakeError(fission.ErrorInvalidArgument, "Function name doesn't match URL")
api.respondWithError(w, err)
return
}
uid, err := api.FunctionStore.Update(&f)
if err != nil {
api.respondWithError(w, err)
return
}
m := &fission.Metadata{Name: f.Name, Uid: uid}
resp, err := json.Marshal(m)
if err != nil {
api.respondWithError(w, err)
return
}
api.respondWithSuccess(w, resp)
}
func (api *API) FunctionApiDelete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var m fission.Metadata
m.Name = vars["function"]
m.Uid = r.FormValue("uid") // empty if uid is absent
if len(m.Uid) == 0 {
log.WithFields(log.Fields{"function": m.Name}).Info("Deleting all versions")
}
err := api.FunctionStore.Delete(m)
if err != nil {
api.respondWithError(w, err)
return
}
api.respondWithSuccess(w, []byte(""))
}
+137
View File
@@ -0,0 +1,137 @@
/*
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 (
log "github.com/Sirupsen/logrus"
"github.com/platform9/fission"
)
type FunctionStore struct {
resourceStore
}
func (fs *FunctionStore) Create(f *fission.Function) (string, error) {
code := []byte(f.Code)
_, uid, err := fs.resourceStore.writeFile(f.Key(), code)
if err != nil {
return "", err
}
f.Metadata.Uid = uid
f.Code = ""
err = fs.resourceStore.create(f)
if err != nil {
fs.resourceStore.deleteFile(f.Key(), uid) // ignore errors
return "", err
}
return f.Metadata.Uid, nil
}
func (fs *FunctionStore) Get(m *fission.Metadata) (*fission.Function, error) {
var f fission.Function
err := fs.resourceStore.read(m.Name, &f)
if err != nil {
return nil, err
}
var code []byte
if len(m.Uid) > 0 {
log.WithFields(log.Fields{"Uid": m.Uid}).Info("fetching by uid")
code, err = fs.resourceStore.readFile(m.Name, &m.Uid)
f.Metadata = *m
} else {
code, err = fs.resourceStore.readFile(m.Name, nil)
}
if err != nil {
return nil, err
}
f.Code = string(code)
return &f, nil
}
func (fs *FunctionStore) Update(f *fission.Function) (string, error) {
code := []byte(f.Code)
_, uid, err := fs.resourceStore.writeFile(f.Key(), code)
if err != nil {
return "", err
}
var fnew fission.Function
err = fs.resourceStore.read(f.Metadata.Name, &fnew)
if err != nil {
fs.resourceStore.deleteFile(f.Key(), uid) // ignore err
return "", err
}
fnew.Metadata.Uid = uid
fnew.Environment = f.Environment
err = fs.resourceStore.update(fnew)
if err != nil {
fs.resourceStore.deleteFile(f.Key(), uid) // ignore err
return "", err
}
return uid, err
}
func (fs *FunctionStore) Delete(m fission.Metadata) error {
if len(m.Uid) == 0 {
err := fs.resourceStore.deleteAllFiles(m.Name)
if err != nil {
return err
}
} else {
err := fs.resourceStore.deleteFile(m.Name, m.Uid)
if err != nil {
return err
}
}
typeName, err := getTypeName(fission.Function{})
if err != nil {
return err
}
return fs.resourceStore.delete(typeName, m.Name)
}
func (fs *FunctionStore) List() ([]fission.Function, error) {
typeName, err := getTypeName(fission.Function{})
if err != nil {
return nil, err
}
bufs, err := fs.resourceStore.getAll(typeName)
if err != nil {
return nil, err
}
js := JsonSerializer{}
functions := make([]fission.Function, 0, len(bufs))
for _, buf := range bufs {
var f fission.Function
err = js.deserialize([]byte(buf), &f)
if err != nil {
return nil, err
}
functions = append(functions, f)
}
return functions, nil
}
+83
View File
@@ -0,0 +1,83 @@
/*
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/satori/go.uuid"
"github.com/platform9/fission"
)
type HTTPTriggerStore struct {
resourceStore
}
func (hts *HTTPTriggerStore) create(ht *fission.HTTPTrigger) error {
ht.Metadata.Uid = uuid.NewV4().String()
return hts.resourceStore.create(ht)
}
func (hts *HTTPTriggerStore) read(m fission.Metadata) (*fission.HTTPTrigger, error) {
var ht fission.HTTPTrigger
err := hts.resourceStore.read(m.Name, &ht)
if err != nil {
return nil, err
}
return &ht, nil
}
func (hts *HTTPTriggerStore) update(ht *fission.HTTPTrigger) error {
err := validateHTTPTrigger(ht)
if err != nil {
return err
}
ht.Metadata.Uid = uuid.NewV4().String()
return hts.resourceStore.update(ht)
}
func (hts *HTTPTriggerStore) delete(m fission.Metadata) error {
typeName, err := getTypeName(fission.HTTPTrigger{})
if err != nil {
return err
}
return hts.resourceStore.delete(typeName, m.Name)
}
func (hts *HTTPTriggerStore) list() ([]fission.HTTPTrigger, error) {
typeName, err := getTypeName(fission.HTTPTrigger{})
if err != nil {
return nil, err
}
bufs, err := hts.resourceStore.getAll(typeName)
if err != nil {
return nil, err
}
triggers := make([]fission.HTTPTrigger, 0, len(bufs))
js := JsonSerializer{}
for _, buf := range bufs {
var ht fission.HTTPTrigger
err = js.deserialize([]byte(buf), &ht)
if err != nil {
return nil, err
}
triggers = append(triggers, ht)
}
return triggers, nil
}
+32
View File
@@ -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)
}
+228
View File
@@ -0,0 +1,228 @@
/*
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"
"reflect"
log "github.com/Sirupsen/logrus"
"github.com/coreos/etcd/client"
"github.com/satori/go.uuid"
"golang.org/x/net/context"
)
type (
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
}
parentKey = "file/" + parentKey
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) {
key = "file/" + key
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 UID " + *uid)
}
}
contents, err := rs.fileStore.read(*uid)
return contents, err
}
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 {
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
}
}
_, err = rs.KeysAPI.Delete(context.Background(), key, &client.DeleteOptions{Dir: true})
return err
}
+147
View File
@@ -0,0 +1,147 @@
/*
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 (
"io/ioutil"
"log"
"os"
"testing"
"github.com/coreos/etcd/client"
"golang.org/x/net/context"
)
type TestResource struct {
A string
B int
}
func (tr TestResource) Key() string {
return tr.A
}
func panicIf(err error) {
if err != nil {
log.Panicf("err: %v", err)
}
}
func assert(b bool, msg string) {
if !b {
log.Panic("assertion failed: " + msg)
}
}
func getTestResourceStore() (*fileStore, client.KeysAPI, *resourceStore) {
// make a tmp dir
dir, err := ioutil.TempDir("", "testFileStore")
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())
// Create
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)
panicIf(err)
// Read
tr1 := TestResource{}
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)
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")
panicIf(err)
res := make([]TestResource, 0, 0)
for _, r := range results {
tmp := TestResource{}
err = s.deserialize([]byte(r), &tmp)
panicIf(err)
res = append(res, tmp)
}
assert(res[0] == tr, "value from retrieved list must equal updated value")
// file tests
fileKey := "resourceStoreTest"
fileContents1 := []byte("hello")
fileContents2 := []byte("world")
key, uid1, err := rs.writeFile(fileKey, fileContents1)
panicIf(err)
defer rs.deleteFile(fileKey, uid1)
log.Printf("key = %v, uid = %v", key, uid1)
// read latest
contents, err := rs.readFile(fileKey, nil)
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)
panicIf(err)
defer rs.deleteFile(fileKey, uid2)
// read latest
contents, err = rs.readFile(fileKey, nil)
panicIf(err)
assert(string(contents) == string(fileContents2), "retrieved file contents must match updated value")
// read by uid
// 1
contents, err = rs.readFile(fileKey, &uid1)
panicIf(err)
assert(string(contents) == string(fileContents1), "retrieved file contents must match updated value")
// 2
contents, err = rs.readFile(fileKey, &uid2)
panicIf(err)
assert(string(contents) == string(fileContents2), "retrieved file contents must match updated value")
}
+28
View File
@@ -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
}
)
+29
View File
@@ -0,0 +1,29 @@
/*
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 fission
import (
"fmt"
)
func (e Error) Error() string {
return fmt.Sprintf("(Error %v) %v", e.Code, e.Message)
}
func MakeError(code int, msg string) Error {
return Error{Code: errorCode(code), Message: msg}
}
+13
View File
@@ -0,0 +1,13 @@
package fission
func (f Function) Key() string {
return f.Metadata.Name
}
func (e Environment) Key() string {
return e.Metadata.Name
}
func (ht HTTPTrigger) Key() string {
return ht.Metadata.Name
}
+28 -14
View File
@@ -18,22 +18,19 @@ package fission
type (
// Metadata is used as the general identifier for all kinds of
// resources managed by the controller. In general, when a
// resource is updated, the Name remains the same, but the UID
// changes. In other words, the UID identifies a particular
// value of a resource.
// resources managed by the controller.
Metadata struct {
Name string
Uid string
Name string `json:"name"`
Uid string `json:"uid,omitempty"`
}
// Function is a unit of executable code. Though it's called
// a function, the code may have more than one function; it's
// usually some sort of module or package.
Function struct {
Metadata
Environment Metadata
Code string
Metadata `json:"metadata"`
Environment Metadata `json:"environment"`
Code string `json:"code"`
}
// Environment identifies the language and OS specific
@@ -42,16 +39,33 @@ type (
// this will also include build containers, as well as support
// tools like debuggers, profilers, etc.
Environment struct {
Metadata
RunContainerImageUrl string
Metadata `json:"metadata"`
RunContainerImageUrl string `json:"runContainerImageUrl"`
}
// HTTPTrigger maps URL patterns to functions. Function.UID
// is optional; if absent, the latest version of the function
// will automatically be selected.
HTTPTrigger struct {
Metadata
UrlPattern string
Function Metadata
Metadata `json:"metadata"`
UrlPattern string `json:"urlpattern"`
Function Metadata `json:"function"`
}
// Errors returned by the Fission API.
Error struct {
Code errorCode `json:"code"`
Message string `json:"message"`
}
errorCode int
)
const (
ErrorInternal = iota
ErrorNotAuthorized
ErrorNotFound
ErrorNameExists
ErrorInvalidArgument
ErrorNoSpace
)