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

CloudEvents integration #404

Merged
merged 64 commits into from
Apr 12, 2018
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
cf400e5
Update Event type struct
RaeesBhatti Mar 29, 2018
81d60cb
go fmt
RaeesBhatti Mar 29, 2018
51df7bd
Fix linting
RaeesBhatti Mar 29, 2018
c0bdb89
Fix a type
RaeesBhatti Mar 29, 2018
a2ce635
Update the schema
RaeesBhatti Mar 30, 2018
905fde5
Update README to reflect the CloudEvent
RaeesBhatti Mar 30, 2018
03fd349
Merge branch 'master' into cloudevents
RaeesBhatti Mar 30, 2018
a5e7cea
Write test for CloudEvents
RaeesBhatti Mar 30, 2018
e9df992
Merge branch 'master' into cloudevents
RaeesBhatti Apr 2, 2018
8ec5042
Update README to move definitons for API
RaeesBhatti Apr 2, 2018
3279ff6
Add reference links to API doc
RaeesBhatti Apr 2, 2018
762cff0
Reformat a bit
RaeesBhatti Apr 3, 2018
ce49e51
Update docs
RaeesBhatti Apr 3, 2018
4fb5991
Docs: Remove CloudEvents definition fields
RaeesBhatti Apr 3, 2018
7060f07
Add link to CloudEvents
RaeesBhatti Apr 3, 2018
579655f
var notation
RaeesBhatti Apr 3, 2018
46f2a6d
Try to parse custom events as CloudEvent
RaeesBhatti Apr 3, 2018
e112c82
Fix CloudEvent parsing
RaeesBhatti Apr 3, 2018
8363564
Update gometalinter cyclo-over param
RaeesBhatti Apr 3, 2018
1625a02
Add validation to Event struct
RaeesBhatti Apr 4, 2018
38978ab
Add default Source because it is required
RaeesBhatti Apr 4, 2018
c2ded5e
Move some checks out of parseCustomEventAsCloudEvent
RaeesBhatti Apr 4, 2018
88f54d7
Revert "Update gometalinter cyclo-over param"
RaeesBhatti Apr 4, 2018
0079e78
Move event parsing to Event.New func
RaeesBhatti Apr 4, 2018
42ad46c
Create NewHTTPEvent func
RaeesBhatti Apr 4, 2018
0c80f71
Fix a test
RaeesBhatti Apr 4, 2018
fd4178c
Change convention
RaeesBhatti Apr 4, 2018
ab0a8a0
Camelcase
RaeesBhatti Apr 4, 2018
ecb4674
Change convention
RaeesBhatti Apr 4, 2018
4400d63
Change comment to reflect that we're using CloudEvents format
RaeesBhatti Apr 4, 2018
a244ad5
Add comment ot NewHTTPEvent
RaeesBhatti Apr 4, 2018
92a8069
Update a comment
RaeesBhatti Apr 4, 2018
57a9646
Update source field in API docs
RaeesBhatti Apr 4, 2018
d477bc0
Validate Event struct
RaeesBhatti Apr 4, 2018
92ec35f
Add tests for event/event.go
RaeesBhatti Apr 4, 2018
c065b69
Correct a test
RaeesBhatti Apr 4, 2018
7bf1dfd
Fix a typo bug in Event.New
RaeesBhatti Apr 4, 2018
8dae643
Revert "Revert "Update gometalinter cyclo-over param""
RaeesBhatti Apr 4, 2018
423a226
Move TransformHeaders function
RaeesBhatti Apr 5, 2018
f6e8f21
add tests for internal http package
Apr 5, 2018
53102a4
add tests for event package
Apr 5, 2018
a44628c
Add extensions field if custom event not cloudevent
RaeesBhatti Apr 6, 2018
8e008f5
Update field names to camelcase
RaeesBhatti Apr 9, 2018
a2f5f24
Update some field names
RaeesBhatti Apr 9, 2018
bfbaeec
Update event example
RaeesBhatti Apr 9, 2018
22a27b0
Accept source param in new event func
RaeesBhatti Apr 9, 2018
614e9c3
Fix linter warning
RaeesBhatti Apr 10, 2018
9b48a3e
Fix test to include tranformationversion anchor
RaeesBhatti Apr 10, 2018
e4984f4
Change a field name
RaeesBhatti Apr 10, 2018
f1df8b5
Declare transformationVersion as const
RaeesBhatti Apr 10, 2018
a5441c4
Fix a loop
RaeesBhatti Apr 10, 2018
ba7969c
Fix tests
RaeesBhatti Apr 10, 2018
e4c9714
Use const
RaeesBhatti Apr 10, 2018
736037a
Use a fixed value for Source
RaeesBhatti Apr 11, 2018
cb04faa
Use pkg constant in test
RaeesBhatti Apr 11, 2018
e025f7e
Update source value in tests
RaeesBhatti Apr 11, 2018
9d0aa38
Update source in docs
RaeesBhatti Apr 11, 2018
00a1ca2
Move TransformationVersion const up
RaeesBhatti Apr 11, 2018
f1df44c
Ignore test generated files
RaeesBhatti Apr 11, 2018
b9b2efc
add more tests
Apr 5, 2018
afc1ba0
minor cleanup
Apr 12, 2018
7de8e5f
add omitempty tag for optional fields
Apr 12, 2018
f7242a3
don't log empty fields
Apr 12, 2018
f4ae257
rename var
Apr 12, 2018
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
18 changes: 7 additions & 11 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,19 @@ for invoking function. By default Events API runs on `:4000` port.

### Event Definition

All data that passes through the Event Gateway is formatted as an Event, based on our default Event schema:

* `event` - `string` - the event name
* `id` - `string` - the event's instance universally unique ID (provided by the event gateway)
* `receivedAt` - `number` - the time (milliseconds) when the Event was received by the Event Gateway (provided by the event gateway)
* `data` - type depends on `dataType` - the event payload
* `dataType` - `string` - the mime type of `data` payload
All data that passes through the Event Gateway is formatted as a CloudEvent, based on [CloudEvents v0.1 schema](https://github.com/cloudevents/spec/blob/master/spec.md):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexdebrie can you talk a final look here. Should we add something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mthenw @elsteelbrain Docs are fine by me


Example:

```json
{
"event": "myapp.user.created",
"id": "66dfc31d-6844-42fd-b1a7-a489a49f65f3",
"receivedAt": 1500897327098,
"eventType": "myapp.user.created",
"eventID": "66dfc31d-6844-42fd-b1a7-a489a49f65f3",
"cloudEventsVersion": "0.1",
"source": "https://slsgateway.com/",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elsteelbrain please update the example

"eventTime": "1990-12-31T23:59:60Z",
"data": { "foo": "bar" },
"dataType": "application/json"
"contentType": "application/json"
}
```

Expand Down
131 changes: 105 additions & 26 deletions event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,136 @@ package event

import (
"encoding/json"
"errors"
"strings"
"time"

"go.uber.org/zap/zapcore"

uuid "github.com/satori/go.uuid"
"github.com/serverless/event-gateway/internal/zap"
validator "gopkg.in/go-playground/validator.v9"
)

// Event is a default event structure. All data that passes through the Event Gateway is formatted as an Event, based on
// this schema.
// Type uniquely identifies an event type.
type Type string

// TypeInvoke is a special type of event for sync function invocation.
const TypeInvoke = Type("invoke")

// TypeHTTP is a special type of event for sync http subscriptions.
const TypeHTTP = Type("http")

const (
mimeJSON = "application/json"
mimeFormMultipart = "multipart/form-data"
mimeFormURLEncoded = "application/x-www-form-urlencoded"
TransformationVersion = "0.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's define it outside of this mime const, under Type consts.

)

// Event is a default event structure. All data that passes through the Event Gateway
// is formatted to a format defined CloudEvents v0.1 spec.
type Event struct {
Type Type `json:"event"`
ID string `json:"id"`
ReceivedAt uint64 `json:"receivedAt"`
Data interface{} `json:"data"`
DataType string `json:"dataType"`
EventType Type `json:"eventType" validate:"required"`
EventTypeVersion string `json:"eventTypeVersion"`
CloudEventsVersion string `json:"cloudEventsVersion" validate:"required"`
Source string `json:"source" validate:"url,required"`
EventID string `json:"eventID" validate:"required"`
EventTime time.Time `json:"eventTime"`
SchemaURL string `json:"schemaURL"`
ContentType string `json:"contentType"`
Extensions zap.MapStringInterface `json:"extensions"`
Data interface{} `json:"data"`
}

// New return new instance of Event.
func New(eventType Type, mime string, payload interface{}) *Event {
return &Event{
Type: eventType,
ID: uuid.NewV4().String(),
ReceivedAt: uint64(time.Now().UnixNano() / int64(time.Millisecond)),
DataType: mime,
Data: payload,
event := &Event{
EventType: eventType,
CloudEventsVersion: "0.1",
Source: "https://serverless.com/event-gateway/#transformationVersion=" + TransformationVersion,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://serverless.com/event-gateway without trailing /

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correct address for the page is https://serverless.com/event-gateway/ with the slash.

EventID: uuid.NewV4().String(),
EventTime: time.Now(),
ContentType: mime,
Data: payload,
}
}

// Type uniquely identifies an event type.
type Type string
// it's a custom event, possibly CloudEvent
if eventType != TypeHTTP && eventType != TypeInvoke {
cloudEvent, err := parseAsCloudEvent(eventType, mime, payload)
if err == nil {
event = cloudEvent
} else {
event.Extensions = zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": TransformationVersion,
},
}
}
}

// TypeInvoke is a special type of event for sync function invocation.
const TypeInvoke = Type("invoke")
// Because event.Data is []bytes here, it will be base64 encoded by default when being sent to remote function,
// which is why we change the event.Data type to "string" for forms, so that, it is left intact.
if eventbody, ok := event.Data.([]byte); ok && len(eventbody) > 0 {
switch {
case mime == mimeJSON:
json.Unmarshal(eventbody, &event.Data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you verify that string won't work here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case event.data.body is stringified JSON (e.g. "{\"some\": \"thing\"}"). Which I think should be the way it should work. The user should parse the JSON themselves just like they would parse a form, rather than us parsing JSON for them and leaving form as is. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be already parsed as this is how AWS Lambda integration with AWS services looks like. That's why we need this json.Unmarshal here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON is kind of first class citizen in EG.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.

case strings.HasPrefix(mime, mimeFormMultipart), mime == mimeFormURLEncoded:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You used ParseMediaType previously. Is it no longer needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is used in different PR. We'll import that when we merge, or do you want me to merge it now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, yeah, let's leave it out here. We will merge it as a part of this second PR.

event.Data = string(eventbody)
}
}

// TypeHTTP is a special type of event for sync http subscriptions.
const TypeHTTP = Type("http")
return event
}

// MarshalLogObject is a part of zapcore.ObjectMarshaler interface
func (e Event) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("type", string(e.Type))
enc.AddString("id", e.ID)
enc.AddUint64("receivedAt", e.ReceivedAt)
enc.AddString("eventType", string(e.EventType))
enc.AddString("eventTypeVersion", e.EventTypeVersion)
enc.AddString("cloudEventsVersion", e.CloudEventsVersion)
enc.AddString("source", e.Source)
enc.AddString("eventID", e.EventID)
enc.AddString("eventTime", e.EventTime.String())
enc.AddString("schemaURL", e.SchemaURL)
enc.AddString("contentType", e.ContentType)
e.Extensions.MarshalLogObject(enc)
payload, _ := json.Marshal(e.Data)
enc.AddString("data", string(payload))
enc.AddString("dataType", e.DataType)

return nil
}

// IsSystem indicates if th event is a system event.
// IsSystem indicates if the event is a system event.
func (e Event) IsSystem() bool {
return strings.HasPrefix(string(e.Type), "gateway.")
return strings.HasPrefix(string(e.EventType), "gateway.")
}

func parseAsCloudEvent(eventType Type, mime string, payload interface{}) (*Event, error) {
if mime != mimeJSON {
return nil, errors.New("content type is not json")
}
body, ok := payload.([]byte)
if ok {
validate := validator.New()

customEvent := &Event{}
err := json.Unmarshal(body, customEvent)
if err != nil {
return nil, err
}

err = validate.Struct(customEvent)
if err != nil {
return nil, err
}

if eventType != customEvent.EventType {
return nil, errors.New("wrong event type")
}

return customEvent, nil
}

return nil, errors.New("couldn't cast to []byte")
}
145 changes: 145 additions & 0 deletions event/event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package event_test

import (
"testing"

eventpkg "github.com/serverless/event-gateway/event"
"github.com/stretchr/testify/assert"
"github.com/serverless/event-gateway/internal/zap"
)

func TestNew(t *testing.T) {
for _, testCase := range newTests {
result := eventpkg.New(testCase.eventType, testCase.mime, testCase.payload)

assert.NotEqual(t, result.EventID, "")
assert.Equal(t, testCase.expectedEvent.EventType, result.EventType)
assert.Equal(t, testCase.expectedEvent.CloudEventsVersion, result.CloudEventsVersion)
assert.Equal(t, testCase.expectedEvent.Source, result.Source)
assert.Equal(t, testCase.expectedEvent.ContentType, result.ContentType)
assert.Equal(t, testCase.expectedEvent.Data, result.Data)
assert.Equal(t, testCase.expectedEvent.Extensions, result.Extensions)
}
}

var newTests = []struct {
eventType eventpkg.Type
mime string
payload interface{}
expectedEvent eventpkg.Event
}{
{ // not CloudEvent
eventpkg.Type("user.created"),
"application/json",
[]byte("test"),
eventpkg.Event{
EventType: eventpkg.Type("user.created"),
CloudEventsVersion: eventpkg.TransformationVersion,
Source: "https://slsgateway.com#transformationVersion=0.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests have to be updated.

ContentType: "application/json",
Data: []byte("test"),
Extensions: zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": eventpkg.TransformationVersion,
},
},
},
},
{ // System event
eventpkg.Type("user.created"),
"application/json",
eventpkg.SystemEventReceivedData{},
eventpkg.Event{
EventType: eventpkg.Type("user.created"),
CloudEventsVersion: eventpkg.TransformationVersion,
Source: "https://slsgateway.com#transformationVersion=0.1",
ContentType: "application/json",
Data: eventpkg.SystemEventReceivedData{},
Extensions: zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": eventpkg.TransformationVersion,
},
},
},
},
{
// valid CloudEvent
eventpkg.Type("user.created"),
"application/json",
[]byte(`{
"eventType": "user.created",
"cloudEventsVersion": "`+ eventpkg.TransformationVersion +`",
"source": "https://example.com/",
"eventID": "6f6ada3b-0aa2-4b3c-989a-91ffc6405f11",
"contentType": "text/plain",
"data": "test"
}`),
eventpkg.Event{
EventType: eventpkg.Type("user.created"),
CloudEventsVersion: eventpkg.TransformationVersion,
Source: "https://example.com/",
ContentType: "text/plain",
Data: "test",
},
},
{
// type mismatch
eventpkg.Type("user.deleted"),
"application/json",
[]byte(`{
"eventType": "user.created",
"cloudEventsVersion": "`+ eventpkg.TransformationVersion +`",
"source": "https://example.com/",
"eventID": "6f6ada3b-0aa2-4b3c-989a-91ffc6405f11",
"contentType": "text/plain",
"data": "test"
}`),
eventpkg.Event{
EventType: eventpkg.Type("user.deleted"),
CloudEventsVersion: eventpkg.TransformationVersion,
Source: "https://slsgateway.com#transformationVersion=0.1",
ContentType: "application/json",
Data: map[string]interface{}{
"eventType": "user.created",
"cloudEventsVersion": eventpkg.TransformationVersion,
"source": "https://example.com/",
"eventID": "6f6ada3b-0aa2-4b3c-989a-91ffc6405f11",
"contentType": "text/plain",
"data": "test",
},
Extensions: zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": eventpkg.TransformationVersion,
},
},
},
},
{
// invalid CloudEvent (missing required fields)
eventpkg.Type("user.created"),
"application/json",
[]byte(`{
"eventType": "user.created",
"cloudEventsVersion": "0.1"
}`),
eventpkg.Event{
EventType: eventpkg.Type("user.created"),
CloudEventsVersion: eventpkg.TransformationVersion,
Source: "https://slsgateway.com#transformationVersion=0.1",
ContentType: "application/json",
Data: map[string]interface{}{
"eventType": "user.created",
"cloudEventsVersion": eventpkg.TransformationVersion,
},
Extensions: zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": eventpkg.TransformationVersion,
},
},
},
},
}
20 changes: 20 additions & 0 deletions event/http.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package event

import (
"net/http"

ihttp "github.com/serverless/event-gateway/internal/http"
)

// HTTPEvent is a event schema used for sending events to HTTP subscriptions.
type HTTPEvent struct {
Headers map[string]string `json:"headers"`
Expand All @@ -10,3 +16,17 @@ type HTTPEvent struct {
Method string `json:"method"`
Params map[string]string `json:"params"`
}

// NewHTTPEvent returns a new instance of HTTPEvent
func NewHTTPEvent(r *http.Request, eventData interface{}) *HTTPEvent {
headers := ihttp.FlattenHeader(r.Header)

return &HTTPEvent{
Headers: headers,
Query: r.URL.Query(),
Body: eventData,
Host: r.Host,
Path: r.URL.Path,
Method: r.Method,
}
}
21 changes: 21 additions & 0 deletions internal/http/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package http

import (
httppkg "net/http"
"strings"
)

// FlattenHeader takes http.Header and flatten value array
// (map[string][]string -> map[string]string) so it's easier
// to access headers by user.
func FlattenHeader(req httppkg.Header) map[string]string {
headers := map[string]string{}
for key, header := range req {
headers[key] = header[0]
if len(header) > 1 {
headers[key] = strings.Join(header, ", ")
}
}

return headers
}
Loading