Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
add support for spaces (#362)
Browse files Browse the repository at this point in the history
  • Loading branch information
mthenw authored Feb 14, 2018
1 parent 36f0712 commit f09cd10
Show file tree
Hide file tree
Showing 31 changed files with 1,084 additions and 406 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ script:
- gometalinter --install --force
- gometalinter --vendor --fast --disable=gotype --disable=vetshadow --disable=gas --skip=mock ./...
- go get github.com/mattn/goveralls
- goveralls -race -service=travis-ci -ignore=*/mock/*,*/*/mock/*
- goveralls -race -service=travis-ci -ignore=./mock/*,./router/mock/*,
- go test ./tests -tags=integration
after_success:
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash
notifications:
Expand Down
52 changes: 40 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ yet ready for production applications._
1. [Components](#components)
1. [Function Discovery](#function-discovery)
1. [Subscriptions](#subscriptions)
1. [Spaces](#spaces)
1. [Events API](#events-api)
1. [Configuration API](#configuration-api)
1. [System Events](#system-events)
Expand Down Expand Up @@ -230,6 +231,25 @@ eventGateway.subscribe({

`listUsers` function will be invoked for every HTTP GET request to `<Events API>/users` endpoint.

### Spaces

One additional concept in the Event Gateway are Spaces. Spaces provide isolation between resources. Space is a
coarse-grained sandbox in which entities can interact freely Functions, and Subscriptions belong to one space. All
actions are possible within a space: publishing, subscribing and invoking. All access cross-space is disabled.

Space is not about access control/authentication/authorization. It's only about isolation. It doesn't enforce any
specific subscription path.

This is how Spaces fit different needs depending on use-case:
* single user - single user uses default space for registering function and creating subscriptions.
* multiple teams/departments - different teams/departments use different spaces for isolation and for hiding internal
implementation and architecture.

Technically speaking Space is a mandatory field ("default" by default) on Function or Subscription object that user has
to provide during function registration or subscription creation. Space is a first class concept in Config API. Config
API can register function in specific space or list all functions or subscriptions from a space.


## Events API

The Event Gateway exposes an API for emitting events. Events API can be used for emitting custom event, HTTP events and
Expand Down Expand Up @@ -370,7 +390,8 @@ Currently, the event gateway supports only string responses.
**Request Headers**

* `Event` - `string` - `"invoke"`
* `Function-ID` - `string` - ID of a function to call
* `Function-ID` - `string` - required, ID of a function to call
* `Space` - `string` - space name, default: `default`

**Request**

Expand Down Expand Up @@ -403,7 +424,8 @@ The Event Gateway exposes a RESTful JSON configuration API. By default Configura

JSON object:

* `functionId` - `string` - required, function name
* `functionId` - `string` - required, function ID
* `space` - `string` - space name, default: `default`
* `provider` - `object` - required, provider specific information about a function, depends on type:
* for AWS Lambda:
* `type` - `string` - required, provider type: `awslambda`
Expand All @@ -425,7 +447,8 @@ Status code:

JSON object:

* `functionId` - `string` - function name
* `functionId` - `string` - function ID
* `space` - `string` - space name
* `provider` - `object` - provider specific information about a function

---
Expand All @@ -434,7 +457,7 @@ JSON object:

**Endpoint**

`PUT <Configuration API URL>/v1/functions/<function id>`
`PUT <Configuration API URL>/v1/functions/<space>/<function id>`

**Request**

Expand Down Expand Up @@ -462,7 +485,8 @@ Status code:

JSON object:

* `functionId` - `string` - function name
* `functionId` - `string` - function ID
* `space` - `string` - space name
* `provider` - `object` - provider specific information about a function

---
Expand All @@ -473,7 +497,7 @@ Delete all types of functions. This operation fails if the function is currently

**Endpoint**

`DELETE <Configuration API URL>/v1/functions/<function id>`
`DELETE <Configuration API URL>/v1/functions/<space>/<function id>`

**Response**

Expand All @@ -488,7 +512,7 @@ Status code:

**Endpoint**

`GET <Configuration API URL>/v1/functions`
`GET <Configuration API URL>/v1/functions/<space>`

**Response**

Expand All @@ -499,7 +523,8 @@ Status code:
JSON object:

* `functions` - `array` of `object` - functions:
* `functionId` - `string` - function name
* `functionId` - `string` - function ID
* `space` - `string` - space name
* `provider` - `object` - provider specific information about a function

### Subscriptions
Expand All @@ -514,6 +539,7 @@ JSON object:

* `event` - `string` - event name
* `functionId` - `string` - ID of function to receive events
* `space` - `string` - space name, default: `default`
* `method` - `string` - optional, in case of `http` event, HTTP method that accepts requests
* `path` - `string` - optional, in case of `http` event, path that accepts requests, it starts with "/"
* `cors` - `object` - optional, in case of `http` event, By default CORS is disabled. When set to empty object CORS configuration will use default values for all fields below. Available fields:
Expand All @@ -533,7 +559,8 @@ JSON object:

* `subscriptionId` - `string` - subscription ID
* `event` - `string` - event name
* `functionId` - ID of function
* `functionId` - function ID
* `space` - `string` - space name
* `method` - `string` - optional, in case of `http` event, HTTP method that accepts requests
* `path` - `string` - optional, in case of `http` event, path that accepts requests, starts with `/`
* `cors` - `object` - optional, in case of `http` event, CORS configuration
Expand All @@ -544,7 +571,7 @@ JSON object:

**Endpoint**

`DELETE <Configuration API URL>/v1/subscriptions/<subscription id>`
`DELETE <Configuration API URL>/v1/subscriptions/<space>/<subscription id>`

**Response**

Expand All @@ -559,7 +586,7 @@ Status code:

**Endpoint**

`GET <Configuration API URL>/v1/subscriptions`
`GET <Configuration API URL>/v1/subscriptions/<space>`

**Response**

Expand All @@ -572,7 +599,8 @@ JSON object:
* `subscriptions` - `array` of `object` - subscriptions
* `subscriptionId` - `string` - subscription ID
* `event` - `string` - event name
* `functionId` - ID of function
* `functionId` - function ID
* `space` - `string` - space name
* `method` - `string` - optional, in case of `http` event, HTTP method that accepts requests
* `path` - `string` - optional, in case of `http` event, path that accepts requests
* `cors` - `object` - optional, in case of `http` event, CORS configuration
Expand Down
5 changes: 2 additions & 3 deletions function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ import (

// Function represents a function deployed on one of the supported providers.
type Function struct {
Space string `json:"space" validate:"required,space"`
ID ID `json:"functionId" validate:"required,functionid"`
Provider *Provider `json:"provider" validate:"required"`
}

// Functions is an array of functions.
type Functions struct {
Functions []*Function `json:"functions"`
}
type Functions []*Function

// ID uniquely identifies a function.
type ID string
Expand Down
8 changes: 4 additions & 4 deletions function/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package function
// Service represents service for managing functions.
type Service interface {
RegisterFunction(fn *Function) (*Function, error)
UpdateFunction(fn *Function) (*Function, error)
GetFunction(id ID) (*Function, error)
GetAllFunctions() ([]*Function, error)
DeleteFunction(id ID) error
UpdateFunction(space string, fn *Function) (*Function, error)
GetFunction(space string, id ID) (*Function, error)
GetFunctions(space string) (Functions, error)
DeleteFunction(space string, id ID) error
}
7 changes: 7 additions & 0 deletions httpapi/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ type ErrMalformedJSON Error
func NewErrMalformedJSON(err error) *ErrMalformedJSON {
return &ErrMalformedJSON{fmt.Sprintf("Malformed JSON payload: %s.", err.Error())}
}

// ErrSpaceMismatch occurs when function couldn't been found in the discovery.
type ErrSpaceMismatch struct{}

func (e ErrSpaceMismatch) Error() string {
return "Object space doesn't match space specified in the URL."
}
68 changes: 44 additions & 24 deletions httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,39 @@ type HTTPAPI struct {
Subscriptions subscription.Service
}

// FunctionsResponse is a HTTPAPI JSON response containing functions.
type FunctionsResponse struct {
Functions function.Functions `json:"functions"`
}

// SubscriptionsResponse is a HTTPAPI JSON response containing subscriptions.
type SubscriptionsResponse struct {
Subscriptions subscription.Subscriptions `json:"subscriptions"`
}

// RegisterRoutes register HTTP API routes
func (h HTTPAPI) RegisterRoutes(router *httprouter.Router) {
router.GET("/v1/status", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {})
router.Handler("GET", "/metrics", promhttp.Handler())

router.GET("/v1/functions/:space/:id", h.getFunction)
router.GET("/v1/functions/:space", h.getFunctions)
router.GET("/v1/functions", h.getFunctions)
router.POST("/v1/functions", h.registerFunction)
router.GET("/v1/functions/:id", h.getFunction)
router.PUT("/v1/functions/:id", h.updateFunction)
router.DELETE("/v1/functions/:id", h.deleteFunction)
router.PUT("/v1/functions/:space/:id", h.updateFunction)
router.DELETE("/v1/functions/:space/:id", h.deleteFunction)

router.POST("/v1/subscriptions", h.createSubscription)
router.DELETE("/v1/subscriptions/*subscriptionID", h.deleteSubscription)
router.GET("/v1/subscriptions/:space", h.getSubscriptions)
router.GET("/v1/subscriptions", h.getSubscriptions)
router.POST("/v1/subscriptions", h.createSubscription)
router.DELETE("/v1/subscriptions/:space/*subscriptionID", h.deleteSubscription)
}

func (h HTTPAPI) getFunction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

fn, err := h.Functions.GetFunction(function.ID(params.ByName("id")))
fn, err := h.Functions.GetFunction(params.ByName("space"), function.ID(params.ByName("id")))
if err != nil {
if _, ok := err.(*function.ErrFunctionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand All @@ -55,12 +67,12 @@ func (h HTTPAPI) getFunctions(w http.ResponseWriter, r *http.Request, params htt
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

fns, err := h.Functions.GetAllFunctions()
fns, err := h.Functions.GetFunctions(params.ByName("space"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}})
} else {
encoder.Encode(&function.Functions{Functions: fns})
encoder.Encode(&FunctionsResponse{fns})
}
}

Expand Down Expand Up @@ -91,6 +103,7 @@ func (h HTTPAPI) registerFunction(w http.ResponseWriter, r *http.Request, params
return
}

w.WriteHeader(http.StatusCreated)
encoder.Encode(output)
}

Expand All @@ -107,8 +120,15 @@ func (h HTTPAPI) updateFunction(w http.ResponseWriter, r *http.Request, params h
return
}

if params.ByName("space") != fn.Space {
w.WriteHeader(http.StatusBadRequest)
responseErr := &ErrSpaceMismatch{}
encoder.Encode(&Response{Errors: []Error{{Message: responseErr.Error()}}})
return
}

fn.ID = function.ID(params.ByName("id"))
output, err := h.Functions.UpdateFunction(fn)
output, err := h.Functions.UpdateFunction(params.ByName("space"), fn)
if err != nil {
if _, ok := err.(*function.ErrFunctionValidation); ok {
w.WriteHeader(http.StatusBadRequest)
Expand All @@ -129,7 +149,7 @@ func (h HTTPAPI) deleteFunction(w http.ResponseWriter, r *http.Request, params h
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

err := h.Functions.DeleteFunction(function.ID(params.ByName("id")))
err := h.Functions.DeleteFunction(params.ByName("space"), function.ID(params.ByName("id")))
if err != nil {
if _, ok := err.(*function.ErrFunctionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand All @@ -143,6 +163,19 @@ func (h HTTPAPI) deleteFunction(w http.ResponseWriter, r *http.Request, params h
}
}

func (h HTTPAPI) getSubscriptions(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

subs, err := h.Subscriptions.GetSubscriptions(params.ByName("space"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}})
} else {
encoder.Encode(&SubscriptionsResponse{subs})
}
}

func (h HTTPAPI) createSubscription(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
Expand Down Expand Up @@ -185,7 +218,7 @@ func (h HTTPAPI) deleteSubscription(w http.ResponseWriter, r *http.Request, para
segments := strings.Split(r.URL.RawPath, "/")
sid := segments[len(segments)-1]

err := h.Subscriptions.DeleteSubscription(subscription.ID(sid))
err := h.Subscriptions.DeleteSubscription(params.ByName("space"), subscription.ID(sid))
if err != nil {
if _, ok := err.(*subscription.ErrSubscriptionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand All @@ -197,16 +230,3 @@ func (h HTTPAPI) deleteSubscription(w http.ResponseWriter, r *http.Request, para
w.WriteHeader(http.StatusNoContent)
}
}

func (h HTTPAPI) getSubscriptions(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

subs, err := h.Subscriptions.GetAllSubscriptions()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}})
} else {
encoder.Encode(&subscription.Subscriptions{Subscriptions: subs})
}
}
Loading

0 comments on commit f09cd10

Please sign in to comment.