Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for spaces #362

Merged
merged 9 commits into from
Feb 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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