diff --git a/controller/api.go b/controller/api.go index 533daa61..5bc9e6d0 100644 --- a/controller/api.go +++ b/controller/api.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "os" + "runtime/debug" log "github.com/Sirupsen/logrus" "github.com/gorilla/handlers" @@ -45,7 +46,7 @@ func (api *API) respondWithSuccess(w http.ResponseWriter, resp []byte) { func (api *API) respondWithError(w http.ResponseWriter, err error) { var code int var msg string - + debug.PrintStack() fe, ok := err.(fission.Error) if ok { msg = fe.Message @@ -83,11 +84,11 @@ func (api *API) serve(port int) { 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("/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") diff --git a/controller/api_test.go b/controller/api_test.go index 988a1841..585f0466 100644 --- a/controller/api_test.go +++ b/controller/api_test.go @@ -17,8 +17,10 @@ limitations under the License. package controller import ( + "flag" "io/ioutil" "net/http" + "os" "testing" "time" @@ -30,20 +32,13 @@ import ( "github.com/platform9/fission/controller/client" ) +var g struct { + client *client.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", @@ -56,35 +51,20 @@ func TestFunctionApi(t *testing.T) { 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) + m, err := g.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) + m, err = g.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) + f, err := g.client.FunctionGet(m) panicIf(err) testFunc.Metadata.Uid = m.Uid @@ -95,7 +75,7 @@ func TestFunctionApi(t *testing.T) { m.Uid = uid2 testFunc.Metadata.Uid = m.Uid testFunc.Code = "code2" - f, err = client.FunctionGet(m) + f, err = g.client.FunctionGet(m) panicIf(err) assert(*f == *testFunc, "second version should match when read by uid") @@ -103,22 +83,99 @@ func TestFunctionApi(t *testing.T) { m.Uid = "" testFunc.Metadata.Uid = uid2 testFunc.Code = "code2" - f, err = client.FunctionGet(m) + f, err = g.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) + m, err = g.client.FunctionCreate(testFunc) panicIf(err) - funcs, err := client.FunctionList() + funcs, err := g.client.FunctionList() panicIf(err) assert(len(funcs) == 2, "created two functions, but didn't find them") - err = client.FunctionDelete(&fission.Metadata{Name: "foo"}) + err = g.client.FunctionDelete(&fission.Metadata{Name: "foo"}) panicIf(err) - err = client.FunctionDelete(&fission.Metadata{Name: "bar"}) + err = g.client.FunctionDelete(&fission.Metadata{Name: "bar"}) panicIf(err) } + +func TestHTTPTriggerApi(t *testing.T) { + testTrigger := &fission.HTTPTrigger{ + Metadata: fission.Metadata{ + Name: "xxx", + Uid: "yyy", + }, + UrlPattern: "/hello", + Function: fission.Metadata{ + Name: "foo", + Uid: "", + }, + } + uid, err := g.client.HTTPTriggerCreate(testTrigger) + panicIf(err) + m := &fission.Metadata{ + Name: testTrigger.Metadata.Name, + Uid: uid, + } + defer g.client.HTTPTriggerDelete(m) + + tr, err := g.client.HTTPTriggerGet(m) + panicIf(err) + testTrigger.Metadata.Uid = m.Uid + assert(*testTrigger == *tr, "trigger should match after reading") + + testTrigger.UrlPattern = "/hi" + uid2, err := g.client.HTTPTriggerUpdate(testTrigger) + panicIf(err) + + m.Uid = uid2 + tr, err = g.client.HTTPTriggerGet(m) + panicIf(err) + testTrigger.Metadata.Uid = m.Uid + assert(*testTrigger == *tr, "trigger should match after reading") + + testTrigger.Metadata.Name = "yyy" + uid, err = g.client.HTTPTriggerCreate(testTrigger) + panicIf(err) + m = &fission.Metadata{ + Name: testTrigger.Metadata.Name, + Uid: uid, + } + defer g.client.HTTPTriggerDelete(m) + + ts, err := g.client.HTTPTriggerList() + panicIf(err) + assert(len(ts) == 2, "created two triggers, but didn't find them") +} + +func TestMain(m *testing.M) { + flag.Parse() + + fileStore, ks, rs := getTestResourceStore() + defer os.RemoveAll(fileStore.root) + + api := &API{ + FunctionStore: FunctionStore{resourceStore: *rs}, + HTTPTriggerStore: HTTPTriggerStore{resourceStore: *rs}, + EnvironmentStore: EnvironmentStore{resourceStore: *rs}, + } + g.client = client.New("http://localhost:8888") + + ks.Delete(context.Background(), "Function", &etcdClient.DeleteOptions{Recursive: true}) + ks.Delete(context.Background(), "HTTPTrigger", &etcdClient.DeleteOptions{Recursive: true}) + + go api.serve(8888) + time.Sleep(500 * time.Millisecond) + + resp, err := http.Get("http://localhost:8888/") + panicIf(err) + assert(resp.StatusCode == 200, "http get status code on root") + _, err = ioutil.ReadAll(resp.Body) + panicIf(err) + + os.Exit(m.Run()) +} diff --git a/controller/client/client.go b/controller/client/client.go index b7725e9e..0ec594ca 100644 --- a/controller/client/client.go +++ b/controller/client/client.go @@ -76,6 +76,26 @@ func (c *Client) url(relativeUrl string) string { return c.Url + "/" + relativeUrl } +func (c *Client) handleResponse(resp *http.Response) ([]byte, error) { + if resp.StatusCode != 200 { + var errCode int + switch resp.StatusCode { + case 403: + errCode = fission.ErrorNotAuthorized + case 404: + errCode = fission.ErrorNotFound + case 400: + errCode = fission.ErrorInvalidArgument + default: + errCode = fission.ErrorInternal + } + return nil, fission.MakeError(errCode, + fmt.Sprintf("HTTP error %v", resp.StatusCode)) + } + body, err := ioutil.ReadAll(resp.Body) + return body, err +} + func (c *Client) FunctionCreate(f *fission.Function) (*fission.Metadata, error) { reqbody, err := json.Marshal(f) if err != nil { @@ -88,17 +108,13 @@ func (c *Client) FunctionCreate(f *fission.Function) (*fission.Metadata, error) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := c.handleResponse(resp) if err != nil { - return nil, err - } - - if resp.StatusCode != 200 { log.WithFields(log.Fields{ - "function": f.Metadata.Name, - "status": resp.StatusCode, + "name": f.Metadata.Name, + "err": err, }).Error("Failed to create function") - return nil, errors.New("failed to create function") + return nil, err } var m fission.Metadata @@ -122,7 +138,7 @@ func (c *Client) FunctionGet(m *fission.Metadata) (*fission.Function, error) { } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := c.handleResponse(resp) if err != nil { return nil, err } @@ -149,7 +165,7 @@ func (c *Client) FunctionUpdate(f *fission.Function) (*fission.Metadata, error) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := c.handleResponse(resp) if err != nil { return nil, err } @@ -177,7 +193,7 @@ func (c *Client) FunctionList() ([]fission.Function, error) { return nil, err } - body, err := ioutil.ReadAll(resp.Body) + body, err := c.handleResponse(resp) if err != nil { return nil, err } @@ -191,16 +207,116 @@ func (c *Client) FunctionList() ([]fission.Function, error) { 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) HTTPTriggerCreate(t *fission.HTTPTrigger) (string, error) { + reqbody, err := json.Marshal(t) + if err != nil { + return "", err + } + + resp, err := http.Post(c.url("triggers/http"), "application/json", bytes.NewReader(reqbody)) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := c.handleResponse(resp) + if err != nil { + log.WithFields(log.Fields{ + "name": t.Metadata.Name, + "err": err, + }).Error("Failed to create http trigger") + return "", err + } + + var m fission.Metadata + err = json.Unmarshal(body, &m) + if err != nil { + return "", err + } + + return m.Uid, nil +} + +func (c *Client) HTTPTriggerGet(m *fission.Metadata) (*fission.HTTPTrigger, error) { + relativeUrl := fmt.Sprintf("triggers/http/%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 := c.handleResponse(resp) + if err != nil { + return nil, err + } + + var t fission.HTTPTrigger + err = json.Unmarshal(body, &t) + if err != nil { + return nil, err + } + + return &t, nil +} + +func (c *Client) HTTPTriggerUpdate(t *fission.HTTPTrigger) (string, error) { + reqbody, err := json.Marshal(t) + if err != nil { + return "", err + } + relativeUrl := fmt.Sprintf("triggers/http/%v", t.Metadata.Name) + + resp, err := c.put(relativeUrl, "application/json", reqbody) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := c.handleResponse(resp) + if err != nil { + return "", err + } + + var m fission.Metadata + err = json.Unmarshal(body, &m) + if err != nil { + return "", err + } + return m.Uid, nil +} + +func (c *Client) HTTPTriggerDelete(m *fission.Metadata) error { + relativeUrl := fmt.Sprintf("triggers/http/%v", m.Name) + if len(m.Uid) > 0 { + relativeUrl += fmt.Sprintf("?uid=%v", m.Uid) + } + err := c.delete(relativeUrl) + return err +} + +func (c *Client) HTTPTriggerList() ([]fission.HTTPTrigger, error) { + resp, err := http.Get(c.url("triggers/http")) + if err != nil { + return nil, err + } + + body, err := c.handleResponse(resp) + if err != nil { + return nil, err + } + + triggers := make([]fission.HTTPTrigger, 0) + err = json.Unmarshal(body, &triggers) + if err != nil { + return nil, err + } + + return triggers, nil +} // func (c *Client) EnvironmentCreate(f *fission.Environment) (string, error) { // } diff --git a/controller/httpTriggerApi.go b/controller/httpTriggerApi.go new file mode 100644 index 00000000..be3a6f26 --- /dev/null +++ b/controller/httpTriggerApi.go @@ -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 ( + "encoding/json" + "io/ioutil" + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/gorilla/mux" + + "github.com/platform9/fission" +) + +func (api *API) HTTPTriggerApiList(w http.ResponseWriter, r *http.Request) { + triggers, err := api.HTTPTriggerStore.List() + if err != nil { + api.respondWithError(w, err) + return + } + + resp, err := json.Marshal(triggers) + if err != nil { + api.respondWithError(w, err) + return + } + + api.respondWithSuccess(w, resp) +} + +func (api *API) HTTPTriggerApiCreate(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + api.respondWithError(w, err) + } + + var t fission.HTTPTrigger + err = json.Unmarshal(body, &t) + if err != nil { + api.respondWithError(w, err) + return + } + + uid, err := api.HTTPTriggerStore.Create(&t) + if err != nil { + api.respondWithError(w, err) + return + } + + m := &fission.Metadata{Name: t.Name, Uid: uid} + resp, err := json.Marshal(m) + if err != nil { + api.respondWithError(w, err) + return + } + + api.respondWithSuccess(w, resp) +} + +func (api *API) HTTPTriggerApiGet(w http.ResponseWriter, r *http.Request) { + var m fission.Metadata + + vars := mux.Vars(r) + m.Name = vars["httpTrigger"] + m.Uid = r.FormValue("uid") // empty if uid is absent + + t, err := api.HTTPTriggerStore.Get(&m) + if err != nil { + api.respondWithError(w, err) + return + } + + resp, err := json.Marshal(t) + if err != nil { + api.respondWithError(w, err) + return + } + + api.respondWithSuccess(w, resp) +} + +func (api *API) HTTPTriggerApiUpdate(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["httpTrigger"] + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + api.respondWithError(w, err) + } + + var t fission.HTTPTrigger + err = json.Unmarshal(body, &t) + if err != nil { + api.respondWithError(w, err) + return + } + + if name != t.Metadata.Name { + err = fission.MakeError(fission.ErrorInvalidArgument, "HTTPTrigger name doesn't match URL") + api.respondWithError(w, err) + return + } + + uid, err := api.HTTPTriggerStore.Update(&t) + if err != nil { + api.respondWithError(w, err) + return + } + + m := &fission.Metadata{Name: t.Name, Uid: uid} + resp, err := json.Marshal(m) + if err != nil { + api.respondWithError(w, err) + return + } + api.respondWithSuccess(w, resp) +} + +func (api *API) HTTPTriggerApiDelete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var m fission.Metadata + m.Name = vars["httpTrigger"] + + m.Uid = r.FormValue("uid") // empty if uid is absent + if len(m.Uid) == 0 { + log.WithFields(log.Fields{"httpTrigger": m.Name}).Info("Deleting all versions") + } + + err := api.HTTPTriggerStore.Delete(m) + if err != nil { + api.respondWithError(w, err) + return + } + + api.respondWithSuccess(w, []byte("")) +} diff --git a/controller/httpTriggerStore.go b/controller/httpTriggerStore.go index 6b78d831..587e64e4 100644 --- a/controller/httpTriggerStore.go +++ b/controller/httpTriggerStore.go @@ -26,12 +26,12 @@ type HTTPTriggerStore struct { resourceStore } -func (hts *HTTPTriggerStore) create(ht *fission.HTTPTrigger) error { +func (hts *HTTPTriggerStore) Create(ht *fission.HTTPTrigger) (string, error) { ht.Metadata.Uid = uuid.NewV4().String() - return hts.resourceStore.create(ht) + return ht.Metadata.Uid, hts.resourceStore.create(ht) } -func (hts *HTTPTriggerStore) read(m fission.Metadata) (*fission.HTTPTrigger, error) { +func (hts *HTTPTriggerStore) Get(m *fission.Metadata) (*fission.HTTPTrigger, error) { var ht fission.HTTPTrigger err := hts.resourceStore.read(m.Name, &ht) if err != nil { @@ -40,12 +40,12 @@ func (hts *HTTPTriggerStore) read(m fission.Metadata) (*fission.HTTPTrigger, err return &ht, nil } -func (hts *HTTPTriggerStore) update(ht *fission.HTTPTrigger) error { +func (hts *HTTPTriggerStore) Update(ht *fission.HTTPTrigger) (string, error) { ht.Metadata.Uid = uuid.NewV4().String() - return hts.resourceStore.update(ht) + return ht.Metadata.Uid, hts.resourceStore.update(ht) } -func (hts *HTTPTriggerStore) delete(m fission.Metadata) error { +func (hts *HTTPTriggerStore) Delete(m fission.Metadata) error { typeName, err := getTypeName(fission.HTTPTrigger{}) if err != nil { return err @@ -53,7 +53,7 @@ func (hts *HTTPTriggerStore) delete(m fission.Metadata) error { return hts.resourceStore.delete(typeName, m.Name) } -func (hts *HTTPTriggerStore) list() ([]fission.HTTPTrigger, error) { +func (hts *HTTPTriggerStore) List() ([]fission.HTTPTrigger, error) { typeName, err := getTypeName(fission.HTTPTrigger{}) if err != nil { return nil, err diff --git a/controller/resourceStore.go b/controller/resourceStore.go index c2ebcbf8..bd6463d6 100644 --- a/controller/resourceStore.go +++ b/controller/resourceStore.go @@ -20,7 +20,7 @@ import ( "errors" "reflect" "time" - + log "github.com/Sirupsen/logrus" "github.com/coreos/etcd/client" "github.com/satori/go.uuid" @@ -40,17 +40,17 @@ func makeResourceStore(fs *fileStore, ks client.KeysAPI, s serializer) *resource } func getEtcdKeyAPI(etcdUrls []string) client.KeysAPI { - cfg := client.Config{ - Endpoints: etcdUrls, - Transport: client.DefaultTransport, - // set timeout per request to fail fast when the target endpoint is unavailable - HeaderTimeoutPerRequest: time.Second, - } - c, err := client.New(cfg) - if err != nil { - log.Fatalf("failed to connect to etcd: %v", err) - } - return client.NewKeysAPI(c) + cfg := client.Config{ + Endpoints: etcdUrls, + Transport: client.DefaultTransport, + // set timeout per request to fail fast when the target endpoint is unavailable + HeaderTimeoutPerRequest: time.Second, + } + c, err := client.New(cfg) + if err != nil { + log.Fatalf("failed to connect to etcd: %v", err) + } + return client.NewKeysAPI(c) } func getTypeName(r resource) (string, error) {