feat: integrate CloudEvents to webhook (#18322)

Integrate CloudEvents as payload format for webhook.

Closes: #17748

Signed-off-by: chlins <chenyuzh@vmware.com>
This commit is contained in:
Chlins Zhang 2023-03-14 10:44:25 +08:00 committed by GitHub
parent 125daf9cdb
commit 65e675d2e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
198 changed files with 17933 additions and 118 deletions

View File

@ -8522,7 +8522,7 @@ definitions:
PayloadFormatType:
type: string
description: The type of webhook paylod format.
example: 'cloudevent'
example: 'CloudEvents'
PayloadFormat:
type: object
description: Webhook supported payload format type collections.

View File

@ -567,8 +567,8 @@ func (c *controller) AddLabel(ctx context.Context, artifactID int64, labelID int
LabelID: labelID,
Ctx: ctx,
}
if err := e.Build(metaData); err == nil {
if err := e.Publish(); err != nil {
if err := e.Build(ctx, metaData); err == nil {
if err := e.Publish(ctx); err != nil {
log.Error(errors.Wrap(err, "mark label to resource handler: event publish"))
}
} else {

View File

@ -81,7 +81,7 @@ func (suite *AuditLogHandlerTestSuite) TestSubscribeTagEvent() {
notifier.Subscribe(event.TopicCreateProject, suite.auditLogHandler)
// event data should implement the interface TopicEvent
ne.BuildAndPublish(&metadata.CreateProjectEventMetadata{
ne.BuildAndPublish(context.TODO(), &metadata.CreateProjectEventMetadata{
ProjectID: 1,
Project: "test",
Operator: "admin",

View File

@ -1,13 +1,13 @@
package util
import (
"context"
"errors"
"fmt"
"strings"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/distribution"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
@ -15,9 +15,9 @@ import (
)
// SendHookWithPolicies send hook by publishing topic of specified target type(notify type)
func SendHookWithPolicies(policies []*policy_model.Policy, payload *notifyModel.Payload, eventType string) error {
func SendHookWithPolicies(ctx context.Context, policies []*policy_model.Policy, payload *notifyModel.Payload, eventType string) error {
// if global notification configured disabled, return directly
if !config.NotificationEnable(orm.Context()) {
if !config.NotificationEnable(ctx) {
log.Debug("notification feature is not enabled")
return nil
}
@ -28,14 +28,15 @@ func SendHookWithPolicies(policies []*policy_model.Policy, payload *notifyModel.
for _, target := range targets {
evt := &event.Event{}
hookMetadata := &event.HookMetaData{
ProjectID: ply.ProjectID,
EventType: eventType,
PolicyID: ply.ID,
Payload: payload,
Target: &target,
}
// It should never affect evaluating other policies when one is failed, but error should return
if err := evt.Build(hookMetadata); err == nil {
if err := evt.Publish(); err != nil {
if err := evt.Build(ctx, hookMetadata); err == nil {
if err := evt.Publish(ctx); err != nil {
errRet = true
log.Errorf("failed to publish hook notify event: %v", err)
}

View File

@ -88,7 +88,7 @@ func (a *Handler) handle(ctx context.Context, event *event.ArtifactEvent) error
return err
}
err = util.SendHookWithPolicies(policies, payload, event.EventType)
err = util.SendHookWithPolicies(ctx, policies, payload, event.EventType)
if err != nil {
return err
}

View File

@ -14,7 +14,6 @@ import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier/model"
proModels "github.com/goharbor/harbor/src/pkg/project/models"
@ -46,12 +45,12 @@ func (r *ReplicationHandler) Handle(ctx context.Context, value interface{}) erro
return fmt.Errorf("nil replication event")
}
payload, project, err := constructReplicationPayload(rpEvent)
payload, project, err := constructReplicationPayload(ctx, rpEvent)
if err != nil {
return err
}
policies, err := notification.PolicyMgr.GetRelatedPolices(orm.Context(), project.ProjectID, rpEvent.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, project.ProjectID, rpEvent.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", rpEvent.EventType, err)
return err
@ -60,7 +59,7 @@ func (r *ReplicationHandler) Handle(ctx context.Context, value interface{}) erro
log.Debugf("cannot find policy for %s event: %v", rpEvent.EventType, rpEvent)
return nil
}
err = util.SendHookWithPolicies(policies, payload, rpEvent.EventType)
err = util.SendHookWithPolicies(ctx, policies, payload, rpEvent.EventType)
if err != nil {
return err
}
@ -72,8 +71,7 @@ func (r *ReplicationHandler) IsStateful() bool {
return false
}
func constructReplicationPayload(event *event.ReplicationEvent) (*model.Payload, *proModels.Project, error) {
ctx := orm.Context()
func constructReplicationPayload(ctx context.Context, event *event.ReplicationEvent) (*model.Payload, *proModels.Project, error) {
task, err := replication.Ctl.GetTask(ctx, event.ReplicationTaskID)
if err != nil {
log.Errorf("failed to get replication task %d: error: %v", event.ReplicationTaskID, err)
@ -191,7 +189,7 @@ func constructReplicationPayload(event *event.ReplicationEvent) (*model.Payload,
payload.EventData.Replication.FailedArtifact = []*ctlModel.ArtifactInfo{failedArtifact}
}
prj, err := project.Ctl.GetByName(orm.Context(), prjName, project.Metadata(true))
prj, err := project.Ctl.GetByName(ctx, prjName, project.Metadata(true))
if err != nil {
log.Errorf("failed to get project %s, error: %v", prjName, err)
return nil, nil, err

View File

@ -15,7 +15,6 @@
package artifact
import (
"context"
"testing"
"time"
@ -28,6 +27,7 @@ import (
repctl "github.com/goharbor/harbor/src/controller/replication"
repctlmodel "github.com/goharbor/harbor/src/controller/replication/model"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
_ "github.com/goharbor/harbor/src/pkg/config/db"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/notification"
@ -109,7 +109,7 @@ func TestReplicationHandler_Handle(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(context.TODO(), tt.args.data)
err := handler.Handle(orm.Context(), tt.args.data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -12,7 +12,6 @@ import (
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
@ -45,7 +44,7 @@ func (r *RetentionHandler) Handle(ctx context.Context, value interface{}) error
return nil
}
payload, dryRun, project, err := r.constructRetentionPayload(trEvent)
payload, dryRun, project, err := r.constructRetentionPayload(ctx, trEvent)
if err != nil {
return err
}
@ -64,7 +63,7 @@ func (r *RetentionHandler) Handle(ctx context.Context, value interface{}) error
log.Debugf("cannot find policy for %s event: %v", trEvent.EventType, trEvent)
return nil
}
err = util.SendHookWithPolicies(policies, payload, trEvent.EventType)
err = util.SendHookWithPolicies(ctx, policies, payload, trEvent.EventType)
if err != nil {
return err
}
@ -76,8 +75,7 @@ func (r *RetentionHandler) IsStateful() bool {
return false
}
func (r *RetentionHandler) constructRetentionPayload(event *event.RetentionEvent) (*model.Payload, bool, int64, error) {
ctx := orm.Context()
func (r *RetentionHandler) constructRetentionPayload(ctx context.Context, event *event.RetentionEvent) (*model.Payload, bool, int64, error) {
task, err := retention.Ctl.GetRetentionExecTask(ctx, event.TaskID)
if err != nil {
log.Errorf("failed to get retention task %d: error: %v", event.TaskID, err)

View File

@ -1,7 +1,6 @@
package artifact
import (
"context"
"os"
"testing"
"time"
@ -14,6 +13,7 @@ import (
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/retention"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/selector"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
@ -97,7 +97,7 @@ func TestRetentionHandler_Handle(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(context.TODO(), tt.args.data)
err := handler.Handle(orm.Context(), tt.args.data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -23,7 +23,6 @@ import (
"github.com/goharbor/harbor/src/controller/event/handler/util"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/notification"
notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model"
proModels "github.com/goharbor/harbor/src/pkg/project/models"
@ -48,7 +47,7 @@ func (qp *Handler) Handle(ctx context.Context, value interface{}) error {
return fmt.Errorf("nil quota event")
}
prj, err := project.Ctl.GetByName(orm.Context(), quotaEvent.Project.Name, project.Metadata(true))
prj, err := project.Ctl.GetByName(ctx, quotaEvent.Project.Name, project.Metadata(true))
if err != nil {
log.Errorf("failed to get project:%s, error: %v", quotaEvent.Project.Name, err)
return err
@ -69,7 +68,7 @@ func (qp *Handler) Handle(ctx context.Context, value interface{}) error {
return err
}
err = util.SendHookWithPolicies(policies, payload, quotaEvent.EventType)
err = util.SendHookWithPolicies(ctx, policies, payload, quotaEvent.EventType)
if err != nil {
return err
}

View File

@ -26,6 +26,7 @@ import (
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/policy"
@ -96,7 +97,7 @@ func (suite *QuotaPreprocessHandlerSuite) TearDownSuite() {
// TestHandle ...
func (suite *QuotaPreprocessHandlerSuite) TestHandle() {
handler := &Handler{}
err := handler.Handle(context.TODO(), suite.evt)
err := handler.Handle(orm.Context(), suite.evt)
suite.NoError(err)
}

View File

@ -18,8 +18,6 @@ import (
"context"
"time"
o "github.com/beego/beego/v2/client/orm"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/event/handler/util"
@ -27,7 +25,6 @@ import (
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier/model"
proModels "github.com/goharbor/harbor/src/pkg/project/models"
@ -66,17 +63,17 @@ func (si *Handler) Handle(ctx context.Context, value interface{}) error {
}
// Get project
prj, err := project.Ctl.Get(orm.Context(), e.Artifact.NamespaceID, project.Metadata(true))
prj, err := project.Ctl.Get(ctx, e.Artifact.NamespaceID, project.Metadata(true))
if err != nil {
return errors.Wrap(err, "scan preprocess handler")
}
payload, err := constructScanImagePayload(e, prj)
payload, err := constructScanImagePayload(ctx, e, prj)
if err != nil {
return errors.Wrap(err, "scan preprocess handler")
}
err = util.SendHookWithPolicies(policies, payload, e.EventType)
err = util.SendHookWithPolicies(ctx, policies, payload, e.EventType)
if err != nil {
return errors.Wrap(err, "scan preprocess handler")
}
@ -89,7 +86,7 @@ func (si *Handler) IsStateful() bool {
return false
}
func constructScanImagePayload(event *event.ScanImageEvent, project *proModels.Project) (*model.Payload, error) {
func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent, project *proModels.Project) (*model.Payload, error) {
repoType := proModels.ProjectPrivate
if project.IsPublic() {
repoType = proModels.ProjectPublic
@ -121,8 +118,6 @@ func constructScanImagePayload(event *event.ScanImageEvent, project *proModels.P
return nil, errors.Wrap(err, "construct scan payload")
}
ctx := orm.NewContext(context.TODO(), o.NewOrm())
art, err := artifact.Ctl.GetByReference(ctx, event.Artifact.Repository, event.Artifact.Digest, nil)
if err != nil {
return nil, err

View File

@ -28,6 +28,7 @@ import (
"github.com/goharbor/harbor/src/controller/event"
sc "github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/policy"
@ -137,7 +138,7 @@ func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() {
func (suite *ScanImagePreprocessHandlerSuite) TestHandle() {
handler := &Handler{}
err := handler.Handle(context.TODO(), suite.evt)
err := handler.Handle(orm.Context(), suite.evt)
suite.NoError(err)
}

View File

@ -239,7 +239,7 @@ func (c *controller) ProxyManifest(ctx context.Context, art lib.ArtifactInfo, re
}
}
if a != nil {
SendPullEvent(a, art.Tag, operator)
SendPullEvent(bCtx, a, art.Tag, operator)
}
}(operator.FromContext(ctx))

View File

@ -152,11 +152,11 @@ func (l *localHelper) CheckDependencies(ctx context.Context, repo string, man di
}
// SendPullEvent send a pull image event
func SendPullEvent(a *artifact.Artifact, tag, operator string) {
func SendPullEvent(ctx context.Context, a *artifact.Artifact, tag, operator string) {
e := &metadata.PullArtifactEventMetadata{
Artifact: &a.Artifact,
Tag: tag,
Operator: operator,
}
event.BuildAndPublish(e)
event.BuildAndPublish(ctx, e)
}

View File

@ -177,9 +177,9 @@ func buildTaskQuery(execID int64, query *q.Query) *q.Query {
func (c *controller) GetLastTriggerTime(ctx context.Context, eventType string, policyID int64) (time.Time, error) {
query := q.New(q.KeyWords{
"vendor_type": webhookJobVendors,
"vendor_id": policyID,
"ExtraAttrs.type": eventType,
"vendor_type": webhookJobVendors,
"vendor_id": policyID,
"ExtraAttrs.event_type": eventType,
})
// fetch the latest execution sort by start_time
execs, err := c.execMgr.List(ctx, query.First(q.NewSort("start_time", true)))

View File

@ -12,6 +12,7 @@ require (
github.com/bmatcuk/doublestar v1.1.1
github.com/casbin/casbin v1.9.1
github.com/cenkalti/backoff/v4 v4.1.2
github.com/cloudevents/sdk-go/v2 v2.13.0
github.com/coreos/go-oidc/v3 v3.0.0
github.com/dghubble/sling v1.1.0
github.com/docker/distribution v2.8.1+incompatible
@ -172,6 +173,8 @@ require (
go.opentelemetry.io/otel/metric v0.22.0 // indirect
go.opentelemetry.io/proto/otlp v0.11.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
google.golang.org/api v0.61.0 // indirect

View File

@ -191,6 +191,8 @@ github.com/beego/beego/v2 v2.0.6 h1:21Aqz3+RzUE1yP9a5xdU6LK54n9Z7NLEJtR4PE7NrPQ=
github.com/beego/beego/v2 v2.0.6/go.mod h1:CH2/JIaB4ceGYVQlYqTAFft4pVk/ol1ZkakUrUvAyns=
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw=
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -236,6 +238,8 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudevents/sdk-go/v2 v2.13.0 h1:2zxDS8RyY1/wVPULGGbdgniGXSzLaRJVl136fLXGsYw=
github.com/cloudevents/sdk-go/v2 v2.13.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUEeUfapHMUX1T5To=
github.com/cloudflare/cfssl v0.0.0-20190510060611-9c027c93ba9e h1:ZtyhUG4s94BMUCdgvRZySr/AXYL5CDcjxhIV/83xJog=
github.com/cloudflare/cfssl v0.0.0-20190510060611-9c027c93ba9e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
@ -409,6 +413,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
@ -1263,6 +1268,7 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@ -1364,9 +1370,12 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
@ -1375,6 +1384,8 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -1440,6 +1451,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -1692,6 +1704,7 @@ golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -1725,6 +1738,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -2,7 +2,7 @@ package notification
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"reflect"
@ -79,11 +79,14 @@ func (sj *SlackJob) Run(ctx job.Context, params job.Parameters) error {
return err
}
sj.logger.Info("start to run slack job")
err := sj.execute(params)
if err != nil {
sj.logger.Error(err)
sj.logger.Errorf("exit slack job, error: %s", err)
} else {
sj.logger.Info("success to run slack job")
}
// Wait a second for slack rate limit, refer to https://api.slack.com/docs/rate-limits
time.Sleep(time.Second)
return err
@ -111,17 +114,25 @@ func (sj *SlackJob) execute(params map[string]interface{}) error {
req, err := http.NewRequest(http.MethodPost, address, bytes.NewReader([]byte(payload)))
if err != nil {
return err
return errors.Wrap(err, "error to generate request")
}
req.Header.Set("Content-Type", "application/json")
sj.logger.Infof("send request to remote endpoint, body: %s", payload)
resp, err := sj.client.Do(req)
if err != nil {
return err
return errors.Wrap(err, "error to send request")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("slack job(target: %s) response code is %d", address, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
sj.logger.Errorf("error to read response body, error: %s", err)
}
return errors.Errorf("abnormal response code: %d, body: %s", resp.StatusCode, string(body))
}
return nil
}

View File

@ -2,13 +2,15 @@ package notification
import (
"bytes"
"fmt"
"encoding/json"
"io"
"net/http"
"os"
"strconv"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/errors"
)
// Max retry has the same meaning as max fails.
@ -58,11 +60,14 @@ func (wj *WebhookJob) Run(ctx job.Context, params job.Parameters) error {
return err
}
wj.logger.Info("start to run webhook job")
if err := wj.execute(ctx, params); err != nil {
wj.logger.Error(err)
wj.logger.Errorf("exit webhook job, error: %s", err)
return err
}
wj.logger.Info("success to run webhook job")
return nil
}
@ -89,20 +94,32 @@ func (wj *WebhookJob) execute(ctx job.Context, params map[string]interface{}) er
req, err := http.NewRequest(http.MethodPost, address, bytes.NewReader([]byte(payload)))
if err != nil {
return err
return errors.Wrap(err, "error to generate request")
}
if v, ok := params["auth_header"]; ok && len(v.(string)) > 0 {
req.Header.Set("Authorization", v.(string))
if h, ok := params["header"].(string); ok {
header := make(http.Header)
if err = json.Unmarshal([]byte(h), &header); err != nil {
return errors.Wrap(err, "error to unmarshal header")
}
req.Header = header
}
req.Header.Set("Content-Type", "application/json")
wj.logger.Infof("send request to remote endpoint, body: %s", payload)
resp, err := wj.client.Do(req)
if err != nil {
return err
return errors.Wrap(err, "error to send request")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook job(target: %s) response code is %d", address, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
wj.logger.Errorf("error to read response body, error: %s", err)
}
return errors.Errorf("abnormal response code: %d, body: %s", resp.StatusCode, string(body))
}
return nil

View File

@ -63,7 +63,7 @@ func TestRun(t *testing.T) {
"skip_cert_verify": true,
"payload": `{"key": "value"}`,
"address": ts.URL,
"auth_header": "auth_test",
"header": `{"Authorization": ["auth_test"]}`,
}
// test correct webhook response
assert.Nil(t, rep.Run(ctx, params))
@ -77,7 +77,7 @@ func TestRun(t *testing.T) {
"skip_cert_verify": true,
"payload": `{"key": "value"}`,
"address": tsWrong.URL,
"auth_header": "auth_test",
"header": `{"Authorization": ["auth_test"]}`,
}
// test incorrect webhook response
assert.NotNil(t, rep.Run(ctx, paramsWrong))

View File

@ -26,6 +26,7 @@ const (
contextKeyArtifactInfo contextKey = "artifactInfo"
contextKeyAuthMode contextKey = "authMode"
contextKeyCarrySession contextKey = "carrySession"
contextKeyRequestID contextKey = "X-Request-ID"
)
// ArtifactInfo wraps the artifact info extracted from the request to "/v2/"
@ -112,3 +113,18 @@ func GetCarrySession(ctx context.Context) bool {
}
return carrySession
}
// WithXRequestID returns a context with XRequestID set
func WithXRequestID(ctx context.Context, version string) context.Context {
return setToContext(ctx, contextKeyRequestID, version)
}
// GetXRequestID gets the XRequestID from the context
func GetXRequestID(ctx context.Context) string {
id := ""
value := getFromContext(ctx, contextKeyRequestID)
if value != nil {
id, _ = value.(string)
}
return id
}

View File

@ -18,6 +18,7 @@ import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
@ -40,3 +41,24 @@ func TestGetAPIVersion(t *testing.T) {
version = GetAPIVersion(ctx)
assert.Equal(t, "1.0", version)
}
func TestSetXRequestID(t *testing.T) {
ctx := WithXRequestID(context.Background(), uuid.NewString())
assert.NotNil(t, ctx)
}
func TestGetXRequestID(t *testing.T) {
// nil context
id := GetXRequestID(nil)
assert.Empty(t, id)
// no request id set in context
id = GetXRequestID(context.Background())
assert.Empty(t, id)
// request id set in context
mockID := uuid.NewString()
ctx := WithXRequestID(context.Background(), mockID)
id = GetXRequestID(ctx)
assert.Equal(t, mockID, id)
}

View File

@ -2,8 +2,6 @@ package hook
import (
"context"
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
@ -34,16 +32,6 @@ func NewHookManager() *DefaultManager {
// StartHook create a webhook job record in database, and submit it to jobservice
func (hm *DefaultManager) StartHook(ctx context.Context, event *model.HookEvent, data *models.JobData) error {
payload, err := json.Marshal(event.Payload)
if err != nil {
return err
}
extraAttrs := make(map[string]interface{})
if err = json.Unmarshal(payload, &extraAttrs); err != nil {
return err
}
var vendorType string
switch event.Target.Type {
case model.NotifyTypeHTTP:
@ -56,11 +44,14 @@ func (hm *DefaultManager) StartHook(ctx context.Context, event *model.HookEvent,
return errors.Errorf("invalid event target type: %s", event.Target.Type)
}
extraAttrs := map[string]interface{}{
"event_type": event.EventType,
"payload": data.Parameters["payload"],
}
// create execution firstly, then create task.
execID, err := hm.execMgr.Create(ctx, vendorType, event.PolicyID, task.ExecutionTriggerEvent, extraAttrs)
if err != nil {
log.Errorf("failed to create execution for webhook based on policy %d: %v", event.PolicyID, err)
return nil
return errors.Errorf("failed to create execution for webhook based on policy %d: %v", event.PolicyID, err)
}
taskID, err := hm.taskMgr.Create(ctx, execID, &task.Job{
@ -69,12 +60,12 @@ func (hm *DefaultManager) StartHook(ctx context.Context, event *model.HookEvent,
JobKind: data.Metadata.JobKind,
},
Parameters: map[string]interface{}(data.Parameters),
}, extraAttrs)
})
if err != nil {
return fmt.Errorf("failed to create the task for webhook based on policy %d: %v", event.PolicyID, err)
return errors.Errorf("failed to create task for webhook based on policy %d: %v", event.PolicyID, err)
}
log.Debugf("created a webhook job %d for the policy %d", taskID, event.PolicyID)
log.Debugf("created webhook task %d for the policy %d", taskID, event.PolicyID)
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/goharbor/harbor/src/pkg/notification/hook"
"github.com/goharbor/harbor/src/pkg/notification/policy"
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/notifier/formats"
notifier_model "github.com/goharbor/harbor/src/pkg/notifier/model"
)
@ -17,6 +18,8 @@ type (
EventType string
// NotifyType is the type of notify
NotifyType string
// PayloadFormatType is the type of payload format
PayloadFormatType string
)
func (e EventType) String() string {
@ -27,6 +30,10 @@ func (n NotifyType) String() string {
return string(n)
}
func (p PayloadFormatType) String() string {
return string(p)
}
var (
// PolicyMgr is a global notification policy manager
PolicyMgr policy.Manager
@ -39,6 +46,9 @@ var (
// supportedNotifyTypes is a slice to store notification type, eg. HTTP, Email etc
supportedNotifyTypes []NotifyType
// supportedPayloadFormatTypes is a slice to store the supported payload formats. eg. Default, CloudEvents etc
supportedPayloadFormatTypes []PayloadFormatType
)
// Init ...
@ -77,6 +87,11 @@ func initSupportedNotifyType() {
for _, notifyType := range notifyTypes {
supportedNotifyTypes = append(supportedNotifyTypes, NotifyType(notifyType))
}
payloadFormats := []string{formats.DefaultFormat, formats.CloudEventsFormat}
for _, payloadFormat := range payloadFormats {
supportedPayloadFormatTypes = append(supportedPayloadFormatTypes, PayloadFormatType(payloadFormat))
}
}
type eventKey struct{}
@ -127,3 +142,7 @@ func GetSupportedEventTypes() []EventType {
func GetSupportedNotifyTypes() []NotifyType {
return supportedNotifyTypes
}
func GetSupportedPayloadFormats() []PayloadFormatType {
return supportedPayloadFormatTypes
}

View File

@ -81,4 +81,5 @@ type EventTarget struct {
Address string `json:"address"`
AuthHeader string `json:"auth_header,omitempty"`
SkipCertVerify bool `json:"skip_cert_verify"`
PayloadFormat string `json:"payload_format,omitempty"`
}

View File

@ -1,6 +1,8 @@
package event
import (
"context"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
@ -38,6 +40,7 @@ type Metadata interface {
// HookMetaData defines hook notification related event data
type HookMetaData struct {
ProjectID int64
PolicyID int64
EventType string
Target *policy_model.EventTarget
@ -47,6 +50,7 @@ type HookMetaData struct {
// Resolve hook metadata into hook event
func (h *HookMetaData) Resolve(evt *Event) error {
data := &model.HookEvent{
ProjectID: h.ProjectID,
PolicyID: h.PolicyID,
EventType: h.EventType,
Target: h.Target,
@ -59,7 +63,7 @@ func (h *HookMetaData) Resolve(evt *Event) error {
}
// Build an event by metadata
func (e *Event) Build(metadata ...Metadata) error {
func (e *Event) Build(ctx context.Context, metadata ...Metadata) error {
for _, md := range metadata {
if err := md.Resolve(e); err != nil {
log.Debugf("failed to resolve event metadata: %v", md)
@ -70,8 +74,8 @@ func (e *Event) Build(metadata ...Metadata) error {
}
// Publish an event
func (e *Event) Publish() error {
if err := notifier.Publish(e.Topic, e.Data); err != nil {
func (e *Event) Publish(ctx context.Context) error {
if err := notifier.Publish(ctx, e.Topic, e.Data); err != nil {
log.Debugf("failed to publish topic %s with event: %v", e.Topic, e.Data)
return errors.Wrap(err, "failed to publish event")
}
@ -80,14 +84,14 @@ func (e *Event) Publish() error {
// BuildAndPublish builds the event according to the metadata and publish the event
// The process is done in a separated goroutine
func BuildAndPublish(metadata ...Metadata) {
func BuildAndPublish(ctx context.Context, metadata ...Metadata) {
go func() {
event := &Event{}
if err := event.Build(metadata...); err != nil {
if err := event.Build(ctx, metadata...); err != nil {
log.Errorf("failed to build the event from metadata: %v", err)
return
}
if err := event.Publish(); err != nil {
if err := event.Publish(ctx); err != nil {
log.Errorf("failed to publish the event %s: %v", event.Topic, err)
return
}

View File

@ -1,6 +1,7 @@
package event
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@ -43,7 +44,7 @@ func TestHookEvent_Build(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := &Event{}
err := event.Build(tt.args.hookMetadata)
err := event.Build(context.TODO(), tt.args.hookMetadata)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return
@ -76,7 +77,7 @@ func TestEvent_Publish(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.event.Publish()
err := tt.args.event.Publish(context.TODO())
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -0,0 +1,147 @@
// Copyright Project Harbor 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 formats
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/uuid"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
var (
// cloudEventsFormatter is the global single formatter for CloudEvents.
cloudEventsFormatter Formatter = &CloudEvents{}
)
func init() {
registerFormats(CloudEventsFormat, cloudEventsFormatter)
}
const (
// CloudEventsFormat is the type for CloudEvents format.
CloudEventsFormat = "CloudEvents"
// extOperator is the key for the operator in the CloudEvents.
extOperator = "operator"
)
var (
// eventTypeMapping defines the mapping of harbor event type and CloudEvents type.
eventTypeMapping = map[string]string{
event.TopicDeleteArtifact: eventType("artifact.deleted"),
event.TopicPullArtifact: eventType("artifact.pulled"),
event.TopicPushArtifact: eventType("artifact.pushed"),
event.TopicQuotaExceed: eventType("quota.exceeded"),
event.TopicQuotaWarning: eventType("quota.warned"),
event.TopicReplication: eventType("replication.status.changed"),
event.TopicScanningFailed: eventType("scan.failed"),
event.TopicScanningCompleted: eventType("scan.completed"),
event.TopicScanningStopped: eventType("scan.stopped"),
event.TopicTagRetention: eventType("tag_retention.finished"),
}
)
// eventType returns the constructed event type.
func eventType(t string) string {
// defines the prefix for event type, organization name or FQDN or more extended possibility,
// use harbor by default.
prefix := "harbor"
return fmt.Sprintf("%s.%s", prefix, t)
}
// CloudEvents is the instance for the CloudEvents format.
type CloudEvents struct{}
// Format implements the interface Formatter.
/*
{
"specversion":"1.0",
"id":"4b2f89a6-548d-4c12-9993-a1f5790b97d2",
"source":"/projects/1/webhook/policies/3",
"type":"harbor.artifact.pulled",
"datacontenttype":"application/json",
"time":"2023-03-06T06:08:43Z",
"data":{
"resources":[
{
"digest":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
"tag":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
"resource_url":"harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c"
}
],
"repository":{
"date_created":1677053165,
"name":"busybox",
"namespace":"library",
"repo_full_name":"library/busybox",
"repo_type":"public"
}
},
"operator":"robot$library+scanner-Trivy-51fe4548-bbe5-11ed-9217-0242ac14000d"
}
*/
func (ce *CloudEvents) Format(ctx context.Context, he *model.HookEvent) (http.Header, []byte, error) {
if he == nil {
return nil, nil, errors.Errorf("HookEvent should not be nil")
}
eventType, ok := eventTypeMapping[he.EventType]
if !ok {
return nil, nil, errors.Errorf("unknown event type: %s", he.EventType)
}
event := cloudevents.NewEvent()
// retrieve request id from context as id, set to uuid if not found
id := lib.GetXRequestID(ctx)
if len(id) == 0 {
id = uuid.NewString()
log.Warningf("cannot extract request id from context, use UUID %s instead", id)
}
event.SetID(id)
event.SetSource(source(he.ProjectID, he.PolicyID))
event.SetType(eventType)
event.SetTime(time.Unix(he.Payload.OccurAt, 0))
event.SetExtension(extOperator, he.Payload.Operator)
if err := event.SetData(cloudevents.ApplicationJSON, he.Payload.EventData); err != nil {
return nil, nil, errors.Wrap(err, "error to set data in CloudEvents")
}
data, err := json.Marshal(event)
if err != nil {
return nil, nil, errors.Wrap(err, "error to marshal CloudEvents")
}
header := http.Header{
"Content-Type": []string{cloudevents.ApplicationCloudEventsJSON},
}
return header, data, nil
}
// source builds the source for CloudEvents.
func source(projectID, policyID int64) string {
return fmt.Sprintf("/projects/%d/webhook/policies/%d", projectID, policyID)
}

View File

@ -0,0 +1,86 @@
// Copyright Project Harbor 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 formats
import (
"context"
"encoding/json"
"net/http"
"testing"
"time"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/stretchr/testify/assert"
)
func TestCloudEvents_Format(t *testing.T) {
ce := &CloudEvents{}
// invalid case
{
header, data, err := ce.Format(nil, nil)
assert.Error(t, err)
assert.Nil(t, header)
assert.Nil(t, data)
}
// normal case
{
he := &model.HookEvent{
ProjectID: 1,
PolicyID: 3,
EventType: "PULL_ARTIFACT",
Payload: &model.Payload{
Type: "PULL_ARTIFACT",
OccurAt: 1678082923,
Operator: "admin",
EventData: &model.EventData{
Resources: []*model.Resource{
{Digest: "sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
Tag: "sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
ResourceURL: "harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
},
},
Repository: &model.Repository{
DateCreated: 1677053165,
Name: "busybox",
Namespace: "library",
RepoFullName: "library/busybox",
RepoType: "public",
},
},
},
}
ctx := context.TODO()
requestID := "mock-request-id"
header, data, err := ce.Format(lib.WithXRequestID(ctx, requestID), he)
assert.NoError(t, err)
assert.Equal(t, http.Header{"Content-Type": []string{"application/cloudevents+json"}}, header)
// validate data format
event := cloudevents.NewEvent()
err = json.Unmarshal(data, &event)
assert.NoError(t, err)
assert.Equal(t, "1.0", event.SpecVersion())
assert.Equal(t, requestID, event.ID())
assert.Equal(t, "/projects/1/webhook/policies/3", event.Source())
assert.Equal(t, "harbor.artifact.pulled", event.Type())
assert.Equal(t, "application/json", event.DataContentType())
assert.Equal(t, "2023-03-06T06:08:43Z", event.Time().Format(time.RFC3339))
assert.Equal(t, "admin", event.Extensions()["operator"])
assert.Equal(t, `{"resources":[{"digest":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c","tag":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c","resource_url":"harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c"}],"repository":{"date_created":1677053165,"name":"busybox","namespace":"library","repo_full_name":"library/busybox","repo_type":"public"}}`, string(event.Data()))
}
}

View File

@ -0,0 +1,83 @@
// Copyright Project Harbor 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 formats
import (
"context"
"encoding/json"
"net/http"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
var (
// defaultFormatter is the global single formatter for Default.
defaultFormatter Formatter = &Default{}
)
func init() {
// for forward compatibility, empty is also the default.
registerFormats("", defaultFormatter)
registerFormats(DefaultFormat, defaultFormatter)
}
const (
// DefaultFormat is the type for default format.
DefaultFormat = "Default"
)
// Default is the instance for default format(original format in previous versions).
type Default struct{}
// Format implements the interface Formatter.
/*
{
"type":"PULL_ARTIFACT",
"occur_at":1678082303,
"operator":"admin",
"event_data":{
"resources":[
{
"digest":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
"tag":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
"resource_url":"harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c"
}
],
"repository":{
"date_created":1677053165,
"name":"busybox",
"namespace":"library",
"repo_full_name":"library/busybox",
"repo_type":"public"
}
}
}
*/
func (d *Default) Format(ctx context.Context, he *model.HookEvent) (http.Header, []byte, error) {
if he == nil {
return nil, nil, errors.Errorf("HookEvent should not be nil")
}
payload, err := json.Marshal(he.Payload)
if err != nil {
return nil, nil, errors.Wrap(err, "error to marshal payload")
}
header := http.Header{
"Content-Type": []string{"application/json"},
}
return header, payload, nil
}

View File

@ -0,0 +1,92 @@
// Copyright Project Harbor 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 formats
import (
"context"
"net/http"
"reflect"
"testing"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
func TestDefault_Format(t *testing.T) {
type args struct {
he *model.HookEvent
}
tests := []struct {
name string
d *Default
args args
want http.Header
want1 []byte
wantErr bool
}{
{
name: "invalid case",
d: &Default{},
args: args{he: nil},
want: nil,
want1: nil,
wantErr: true,
},
{
name: "normal case",
d: &Default{},
args: args{he: &model.HookEvent{
Payload: &model.Payload{
Type: "PULL_ARTIFACT",
OccurAt: 1678082303,
Operator: "admin",
EventData: &model.EventData{
Resources: []*model.Resource{
{Digest: "sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
Tag: "sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
ResourceURL: "harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c",
},
},
Repository: &model.Repository{
DateCreated: 1677053165,
Name: "busybox",
Namespace: "library",
RepoFullName: "library/busybox",
RepoType: "public",
},
},
},
}},
want: http.Header{"Content-Type": []string{"application/json"}},
want1: []byte(`{"type":"PULL_ARTIFACT","occur_at":1678082303,"operator":"admin","event_data":{"resources":[{"digest":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c","tag":"sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c","resource_url":"harbor.dev/library/busybox@sha256:dde8e930c7b6a490f728e66292bc9bce42efc9bbb5278bae40e4f30f6e00fe8c"}],"repository":{"date_created":1677053165,"name":"busybox","namespace":"library","repo_full_name":"library/busybox","repo_type":"public"}}}`),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &Default{}
got, got1, err := d.Format(context.TODO(), tt.args.he)
if (err != nil) != tt.wantErr {
t.Errorf("Default.Format() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Default.Format() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("Default.Format() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@ -0,0 +1,51 @@
// Copyright Project Harbor 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 formats
import (
"context"
"net/http"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
// formatsRegistry is the service registry for formats.
var formatsRegistry map[string]Formatter
// registerFormats registers the format to formatsRegistry.
func registerFormats(formatType string, formatter Formatter) {
if formatsRegistry == nil {
formatsRegistry = make(map[string]Formatter)
}
formatsRegistry[formatType] = formatter
}
// Formatter is the interface for event which for implementing different drivers to
// organize their customize data format.
type Formatter interface {
// Format formats the data to expected format and return request headers and encoded payload
Format(context.Context, *model.HookEvent) (http.Header, []byte, error)
}
// GetFormatter returns corresponding formatter from format type.
func GetFormatter(formatType string) (Formatter, error) {
if formatter, ok := formatsRegistry[formatType]; ok {
return formatter, nil
}
return nil, errors.Errorf("unknown format type: %s", formatType)
}

View File

@ -3,12 +3,12 @@ package notification
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier/formats"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
@ -47,17 +47,33 @@ func (h *HTTPHandler) process(ctx context.Context, event *model.HookEvent) error
}
j.Name = job.WebhookJobVendorType
payload, err := json.Marshal(event.Payload)
if event == nil || event.Payload == nil || event.Target == nil {
return errors.Errorf("invalid event: %+v", event)
}
formatter, err := formats.GetFormatter(event.Target.PayloadFormat)
if err != nil {
return fmt.Errorf("marshal from payload %v failed: %v", event.Payload, err)
return errors.Wrap(err, "error to get formatter")
}
header, payload, err := formatter.Format(ctx, event)
if err != nil {
return errors.Wrap(err, "error to format event")
}
if len(event.Target.AuthHeader) > 0 {
header.Set("Authorization", event.Target.AuthHeader)
}
headerBytes, err := json.Marshal(header)
if err != nil {
return errors.Wrap(err, "error to marshal header")
}
j.Parameters = map[string]interface{}{
"payload": string(payload),
"address": event.Target.Address,
// Users can define a auth header in http statement in notification(webhook) policy.
// So it will be sent in header in http request.
"auth_header": event.Target.AuthHeader,
"payload": string(payload),
"address": event.Target.Address,
"header": string(headerBytes),
"skip_cert_verify": event.Target.SkipCertVerify,
}
return notification.HookManager.StartHook(ctx, event, j)

View File

@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
@ -16,7 +15,6 @@ import (
)
func TestSlackHandler_Handle(t *testing.T) {
dao.PrepareTestForPostgresSQL()
hookMgr := notification.HookManager
defer func() {
notification.HookManager = hookMgr

View File

@ -7,6 +7,7 @@ import (
// HookEvent is hook related event data to publish
type HookEvent struct {
ProjectID int64
PolicyID int64
EventType string
Target *policy_model.EventTarget

View File

@ -1,6 +1,7 @@
package notifier
import (
"context"
"errors"
"fmt"
"reflect"
@ -160,7 +161,7 @@ func (nw *NotificationWatcher) UnHandle(topic string, handler string) error {
}
// Notify that notification is coming.
func (nw *NotificationWatcher) Notify(notification Notification) error {
func (nw *NotificationWatcher) Notify(ctx context.Context, notification Notification) error {
if strings.TrimSpace(notification.Topic) == "" {
return errors.New("empty topic can not be notified")
}
@ -198,7 +199,7 @@ func (nw *NotificationWatcher) Notify(notification Notification) error {
<-ch
}
}()
if err := hd.Handle(orm.Context(), notification.Value); err != nil {
if err := hd.Handle(orm.Copy(ctx), notification.Value); err != nil {
// Currently, we just log the error
log.Errorf("Error occurred when triggering handler %s of topic %s: %s\n", reflect.TypeOf(hd).String(), notification.Topic, err.Error())
} else {
@ -222,8 +223,8 @@ func UnSubscribe(topic string, handler string) error {
}
// Publish is a wrapper utility method for NotificationWatcher.notify()
func Publish(topic string, value interface{}) error {
return notificationWatcher.Notify(Notification{
func Publish(ctx context.Context, topic string, value interface{}) error {
return notificationWatcher.Notify(ctx, Notification{
Topic: topic,
Value: value,
})

View File

@ -135,8 +135,8 @@ func TestPublish(t *testing.T) {
t.Fail()
}
Publish("topic1", 100)
Publish("topic2", 50)
Publish(context.TODO(), "topic1", 100)
Publish(context.TODO(), "topic2", 50)
// Waiting for async is done
<-time.After(1 * time.Second)
@ -174,7 +174,7 @@ func TestConcurrentPublish(t *testing.T) {
// Publish in a short interval.
for i := 0; i < 10; i++ {
Publish("topic1", 100)
Publish(context.TODO(), "topic1", 100)
}
// Waiting for async is done

View File

@ -58,8 +58,8 @@ func retentionTaskCheckInProcessor(ctx context.Context, t *task.Task, sc *job.St
TaskID: taskID,
}
if err := e.Build(metaData); err == nil {
if err := e.Publish(); err != nil {
if err := e.Build(ctx, metaData); err == nil {
if err := e.Publish(ctx); err != nil {
log.G(ctx).WithField("error", err).Errorf("tag retention job hook handler: event publish")
}
} else {

View File

@ -31,7 +31,7 @@ func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler
next.ServeHTTP(res, r.WithContext(notification.NewContext(r.Context(), evc)))
if res.Success() || evc.MustNotify {
for e := evc.Events.Front(); e != nil; e = e.Next() {
event.BuildAndPublish(e.Value.(event.Metadata))
event.BuildAndPublish(r.Context(), e.Value.(event.Metadata))
}
}
}, skippers...)

View File

@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
"github.com/goharbor/harbor/src/lib"
tracelib "github.com/goharbor/harbor/src/lib/trace"
"github.com/goharbor/harbor/src/server/middleware"
)
@ -40,6 +41,8 @@ func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler
if tracelib.Enabled() {
oteltrace.SpanFromContext(r.Context()).SetAttributes(attribute.Key(HeaderXRequestID).String(rid))
}
next.ServeHTTP(w, r)
// also set the request id to context
ctx := lib.WithXRequestID(r.Context(), rid)
next.ServeHTTP(w, r.WithContext(ctx))
}, skippers...)
}

View File

@ -1,12 +1,9 @@
package model
import (
"encoding/json"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/task"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
@ -36,14 +33,12 @@ func (n *WebhookJob) ToSwagger() *models.WebhookJob {
webhookJob.NotifyType = notifyType
if n.ExtraAttrs != nil {
if eventType, ok := n.ExtraAttrs["type"].(string); ok {
if eventType, ok := n.ExtraAttrs["event_type"].(string); ok {
webhookJob.EventType = eventType
}
detail, err := json.Marshal(n.ExtraAttrs)
if err == nil {
webhookJob.JobDetail = string(detail)
} else {
log.Errorf("failed to marshal exec.ExtraAttrs, error: %v", err)
if payload, ok := n.ExtraAttrs["payload"].(string); ok {
webhookJob.JobDetail = payload
}
}

View File

@ -37,6 +37,7 @@ func (n *WebhookPolicy) ToTargets() []*models.WebhookTargetObject {
Address: t.Address,
AuthHeader: t.AuthHeader,
SkipCertVerify: t.SkipCertVerify,
PayloadFormat: models.PayloadFormatType(t.PayloadFormat),
})
}
return results

View File

@ -392,6 +392,12 @@ func (n *webhookAPI) GetSupportedEventTypes(ctx context.Context, params webhook.
for _, eventType := range notification.GetSupportedEventTypes() {
notificationTypes.EventType = append(notificationTypes.EventType, models.EventType(eventType))
}
// currently only http type support payload format
httpPayloadFormats := &models.PayloadFormat{NotifyType: models.NotifyType("http")}
for _, formatType := range notification.GetSupportedPayloadFormats() {
httpPayloadFormats.Formats = append(httpPayloadFormats.Formats, models.PayloadFormatType(formatType))
}
notificationTypes.PayloadFormats = []*models.PayloadFormat{httpPayloadFormats}
return webhook.NewGetSupportedEventTypesOK().WithPayload(notificationTypes)
}
@ -411,6 +417,15 @@ func (n *webhookAPI) validateTargets(policy *policy_model.Policy) (bool, error)
if !isNotifyTypeSupported(target.Type) {
return false, errors.New(nil).WithMessage("unsupported target type %s with policy %s", target.Type, policy.Name).WithCode(errors.BadRequestCode)
}
// don't allow set the payload format for slack type
// slack should be migrated as a kind of payload in the future
if len(target.PayloadFormat) > 0 && target.Type == "slack" {
return false, errors.New(nil).WithMessage("set payload format is not allowed for slack").WithCode(errors.BadRequestCode)
}
if len(target.PayloadFormat) > 0 && !isPayloadFormatSupported(target.PayloadFormat) {
return false, errors.New(nil).WithMessage("unsupported payload format type: %s", target.PayloadFormat).WithCode(errors.BadRequestCode)
}
}
return true, nil
}
@ -475,3 +490,13 @@ func isNotifyTypeSupported(notifyType string) bool {
return false
}
func isPayloadFormatSupported(payloadFormat string) bool {
for _, t := range notification.GetSupportedPayloadFormats() {
if t.String() == payloadFormat {
return true
}
}
return false
}

201
src/vendor/github.com/cloudevents/sdk-go/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

181
src/vendor/github.com/cloudevents/sdk-go/v2/alias.go generated vendored Normal file
View File

@ -0,0 +1,181 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
// Package v2 reexports a subset of the SDK v2 API.
package v2
// Package cloudevents alias' common functions and types to improve discoverability and reduce
// the number of imports for simple HTTP clients.
import (
"github.com/cloudevents/sdk-go/v2/binding"
"github.com/cloudevents/sdk-go/v2/client"
"github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
"github.com/cloudevents/sdk-go/v2/protocol/http"
"github.com/cloudevents/sdk-go/v2/types"
)
// Client
type ClientOption = client.Option
type Client = client.Client
// Event
type Event = event.Event
type Result = protocol.Result
// Context
type EventContext = event.EventContext
type EventContextV1 = event.EventContextV1
type EventContextV03 = event.EventContextV03
// Custom Types
type Timestamp = types.Timestamp
type URIRef = types.URIRef
// HTTP Protocol
type HTTPOption = http.Option
type HTTPProtocol = http.Protocol
// Encoding
type Encoding = binding.Encoding
// Message
type Message = binding.Message
const (
// ReadEncoding
ApplicationXML = event.ApplicationXML
ApplicationJSON = event.ApplicationJSON
TextPlain = event.TextPlain
ApplicationCloudEventsJSON = event.ApplicationCloudEventsJSON
ApplicationCloudEventsBatchJSON = event.ApplicationCloudEventsBatchJSON
Base64 = event.Base64
// Event Versions
VersionV1 = event.CloudEventsVersionV1
VersionV03 = event.CloudEventsVersionV03
// Encoding
EncodingBinary = binding.EncodingBinary
EncodingStructured = binding.EncodingStructured
)
var (
// ContentType Helpers
StringOfApplicationJSON = event.StringOfApplicationJSON
StringOfApplicationXML = event.StringOfApplicationXML
StringOfTextPlain = event.StringOfTextPlain
StringOfApplicationCloudEventsJSON = event.StringOfApplicationCloudEventsJSON
StringOfApplicationCloudEventsBatchJSON = event.StringOfApplicationCloudEventsBatchJSON
StringOfBase64 = event.StringOfBase64
// Client Creation
NewClient = client.New
NewClientHTTP = client.NewHTTP
// Deprecated: please use New with the observability options.
NewClientObserved = client.NewObserved
// Deprecated: Please use NewClientHTTP with the observability options.
NewDefaultClient = client.NewDefault
NewHTTPReceiveHandler = client.NewHTTPReceiveHandler
// Client Options
WithEventDefaulter = client.WithEventDefaulter
WithUUIDs = client.WithUUIDs
WithTimeNow = client.WithTimeNow
// Deprecated: this is now noop and will be removed in future releases.
WithTracePropagation = client.WithTracePropagation()
// Event Creation
NewEvent = event.New
// Results
NewResult = protocol.NewResult
ResultIs = protocol.ResultIs
ResultAs = protocol.ResultAs
// Receipt helpers
NewReceipt = protocol.NewReceipt
ResultACK = protocol.ResultACK
ResultNACK = protocol.ResultNACK
IsACK = protocol.IsACK
IsNACK = protocol.IsNACK
IsUndelivered = protocol.IsUndelivered
// HTTP Results
NewHTTPResult = http.NewResult
NewHTTPRetriesResult = http.NewRetriesResult
// Message Creation
ToMessage = binding.ToMessage
// Event Creation
NewEventFromHTTPRequest = http.NewEventFromHTTPRequest
NewEventFromHTTPResponse = http.NewEventFromHTTPResponse
// HTTP Messages
WriteHTTPRequest = http.WriteRequest
// Context
ContextWithTarget = context.WithTarget
TargetFromContext = context.TargetFrom
ContextWithRetriesConstantBackoff = context.WithRetriesConstantBackoff
ContextWithRetriesLinearBackoff = context.WithRetriesLinearBackoff
ContextWithRetriesExponentialBackoff = context.WithRetriesExponentialBackoff
WithEncodingBinary = binding.WithForceBinary
WithEncodingStructured = binding.WithForceStructured
// Custom Types
ParseTimestamp = types.ParseTimestamp
ParseURIRef = types.ParseURIRef
ParseURI = types.ParseURI
// HTTP Protocol
NewHTTP = http.New
// HTTP Protocol Options
WithTarget = http.WithTarget
WithHeader = http.WithHeader
WithShutdownTimeout = http.WithShutdownTimeout
//WithEncoding = http.WithEncoding
//WithStructuredEncoding = http.WithStructuredEncoding // TODO: expose new way
WithPort = http.WithPort
WithPath = http.WithPath
WithMiddleware = http.WithMiddleware
WithListener = http.WithListener
WithRoundTripper = http.WithRoundTripper
WithGetHandlerFunc = http.WithGetHandlerFunc
WithOptionsHandlerFunc = http.WithOptionsHandlerFunc
WithDefaultOptionsHandlerFunc = http.WithDefaultOptionsHandlerFunc
)

View File

@ -0,0 +1,52 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"context"
"io"
"github.com/cloudevents/sdk-go/v2/binding/spec"
)
// MessageMetadataWriter is used to set metadata when a binary Message is visited.
type MessageMetadataWriter interface {
// Set a standard attribute.
//
// The value can either be the correct golang type for the attribute, or a canonical
// string encoding, or nil. If value is nil, then the attribute should be deleted.
// See package types to perform the needed conversions.
SetAttribute(attribute spec.Attribute, value interface{}) error
// Set an extension attribute.
//
// The value can either be the correct golang type for the attribute, or a canonical
// string encoding, or nil. If value is nil, then the extension should be deleted.
// See package types to perform the needed conversions.
SetExtension(name string, value interface{}) error
}
// BinaryWriter is used to visit a binary Message and generate a new representation.
//
// Protocols that supports binary encoding should implement this interface to implement direct
// binary to binary encoding and event to binary encoding.
//
// Start() and End() methods must be invoked by the caller of Message.ReadBinary() every time
// the BinaryWriter implementation is used to visit a Message.
type BinaryWriter interface {
MessageMetadataWriter
// Method invoked at the beginning of the visit. Useful to perform initial memory allocations
Start(ctx context.Context) error
// SetData receives an io.Reader for the data attribute.
// io.Reader is not invoked when the data attribute is empty
SetData(data io.Reader) error
// End method is invoked only after the whole encoding process ends successfully.
// If it fails, it's never invoked. It can be used to finalize the message.
End(ctx context.Context) error
}

View File

@ -0,0 +1,68 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package binding defines interfaces for protocol bindings.
NOTE: Most applications that emit or consume events should use the ../client
package, which provides a simpler API to the underlying binding.
The interfaces in this package provide extra encoding and protocol information
to allow efficient forwarding and end-to-end reliable delivery between a
Receiver and a Sender belonging to different bindings. This is useful for
intermediary applications that route or forward events, but not necessary for
most "endpoint" applications that emit or consume events.
Protocol Bindings
A protocol binding usually implements a Message, a Sender and Receiver, a StructuredWriter and a BinaryWriter (depending on the supported encodings of the protocol) and an Write[ProtocolMessage] method.
Read and write events
The core of this package is the binding.Message interface.
Through binding.MessageReader It defines how to read a protocol specific message for an
encoded event in structured mode or binary mode.
The entity who receives a protocol specific data structure representing a message
(e.g. an HttpRequest) encapsulates it in a binding.Message implementation using a NewMessage method (e.g. http.NewMessage).
Then the entity that wants to send the binding.Message back on the wire,
translates it back to the protocol specific data structure (e.g. a Kafka ConsumerMessage), using
the writers BinaryWriter and StructuredWriter specific to that protocol.
Binding implementations exposes their writers
through a specific Write[ProtocolMessage] function (e.g. kafka.EncodeProducerMessage),
in order to simplify the encoding process.
The encoding process can be customized in order to mutate the final result with binding.TransformerFactory.
A bunch of these are provided directly by the binding/transformer module.
Usually binding.Message implementations can be encoded only one time, because the encoding process drain the message itself.
In order to consume a message several times, the binding/buffering package provides several APIs to buffer the Message.
A message can be converted to an event.Event using binding.ToEvent() method.
An event.Event can be used as Message casting it to binding.EventMessage.
In order to simplify the encoding process for each protocol, this package provide several utility methods like binding.Write and binding.DirectWrite.
The binding.Write method tries to preserve the structured/binary encoding, in order to be as much efficient as possible.
Messages can be eventually wrapped to change their behaviours and binding their lifecycle, like the binding.FinishMessage.
Every Message wrapper implements the MessageWrapper interface
Sender and Receiver
A Receiver receives protocol specific messages and wraps them to into binding.Message implementations.
A Sender converts arbitrary Message implementations to a protocol-specific form using the protocol specific Write method
and sends them.
Message and ExactlyOnceMessage provide methods to allow acknowledgments to
propagate when a reliable messages is forwarded from a Receiver to a Sender.
QoS 0 (unreliable), 1 (at-least-once) and 2 (exactly-once) are supported.
Transport
A binding implementation providing Sender and Receiver implementations can be used as a Transport through the BindingTransport adapter.
*/
package binding

View File

@ -0,0 +1,45 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import "errors"
// Encoding enum specifies the type of encodings supported by binding interfaces
type Encoding int
const (
// Binary encoding as specified in https://github.com/cloudevents/spec/blob/master/spec.md#message
EncodingBinary Encoding = iota
// Structured encoding as specified in https://github.com/cloudevents/spec/blob/master/spec.md#message
EncodingStructured
// Message is an instance of EventMessage or it contains EventMessage nested (through MessageWrapper)
EncodingEvent
// When the encoding is unknown (which means that the message is a non-event)
EncodingUnknown
)
func (e Encoding) String() string {
switch e {
case EncodingBinary:
return "binary"
case EncodingStructured:
return "structured"
case EncodingEvent:
return "event"
case EncodingUnknown:
return "unknown"
}
return ""
}
// ErrUnknownEncoding specifies that the Message is not an event or it is encoded with an unknown encoding
var ErrUnknownEncoding = errors.New("unknown Message encoding")
// ErrNotStructured returned by Message.Structured for non-structured messages.
var ErrNotStructured = errors.New("message is not in structured mode")
// ErrNotBinary returned by Message.Binary for non-binary messages.
var ErrNotBinary = errors.New("message is not in binary mode")

View File

@ -0,0 +1,108 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"bytes"
"context"
"github.com/cloudevents/sdk-go/v2/binding/format"
"github.com/cloudevents/sdk-go/v2/binding/spec"
"github.com/cloudevents/sdk-go/v2/event"
)
type eventFormatKey int
const (
formatEventStructured eventFormatKey = iota
)
// EventMessage type-converts a event.Event object to implement Message.
// This allows local event.Event objects to be sent directly via Sender.Send()
// s.Send(ctx, binding.EventMessage(e))
// When an event is wrapped into a EventMessage, the original event could be
// potentially mutated. If you need to use the Event again, after wrapping it into
// an Event message, you should copy it before
type EventMessage event.Event
func ToMessage(e *event.Event) Message {
return (*EventMessage)(e)
}
func (m *EventMessage) ReadEncoding() Encoding {
return EncodingEvent
}
func (m *EventMessage) ReadStructured(ctx context.Context, builder StructuredWriter) error {
f := GetOrDefaultFromCtx(ctx, formatEventStructured, format.JSON).(format.Format)
b, err := f.Marshal((*event.Event)(m))
if err != nil {
return err
}
return builder.SetStructuredEvent(ctx, f, bytes.NewReader(b))
}
func (m *EventMessage) ReadBinary(ctx context.Context, b BinaryWriter) (err error) {
err = eventContextToBinaryWriter(m.Context, b)
if err != nil {
return err
}
// Pass the body
body := (*event.Event)(m).Data()
if len(body) > 0 {
err = b.SetData(bytes.NewBuffer(body))
if err != nil {
return err
}
}
return nil
}
func (m *EventMessage) GetAttribute(k spec.Kind) (spec.Attribute, interface{}) {
sv := spec.VS.Version(m.Context.GetSpecVersion())
a := sv.AttributeFromKind(k)
if a != nil {
return a, a.Get(m.Context)
}
return nil, nil
}
func (m *EventMessage) GetExtension(name string) interface{} {
ext, _ := m.Context.GetExtension(name)
return ext
}
func eventContextToBinaryWriter(c event.EventContext, b BinaryWriter) (err error) {
// Pass all attributes
sv := spec.VS.Version(c.GetSpecVersion())
for _, a := range sv.Attributes() {
value := a.Get(c)
if value != nil {
err = b.SetAttribute(a, value)
}
if err != nil {
return err
}
}
// Pass all extensions
for k, v := range c.GetExtensions() {
err = b.SetExtension(k, v)
if err != nil {
return err
}
}
return nil
}
func (*EventMessage) Finish(error) error { return nil }
var _ Message = (*EventMessage)(nil) // Test it conforms to the interface
var _ MessageMetadataReader = (*EventMessage)(nil) // Test it conforms to the interface
// UseFormatForEvent configures which format to use when marshalling the event to structured mode
func UseFormatForEvent(ctx context.Context, f format.Format) context.Context {
return context.WithValue(ctx, formatEventStructured, f)
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import "github.com/cloudevents/sdk-go/v2/binding/spec"
type finishMessage struct {
Message
finish func(error)
}
func (m *finishMessage) GetAttribute(k spec.Kind) (spec.Attribute, interface{}) {
return m.Message.(MessageMetadataReader).GetAttribute(k)
}
func (m *finishMessage) GetExtension(s string) interface{} {
return m.Message.(MessageMetadataReader).GetExtension(s)
}
func (m *finishMessage) GetWrappedMessage() Message {
return m.Message
}
func (m *finishMessage) Finish(err error) error {
err2 := m.Message.Finish(err) // Finish original message first
if m.finish != nil {
m.finish(err) // Notify callback
}
return err2
}
var _ MessageWrapper = (*finishMessage)(nil)
// WithFinish returns a wrapper for m that calls finish() and
// m.Finish() in its Finish().
// Allows code to be notified when a message is Finished.
func WithFinish(m Message, finish func(error)) Message {
return &finishMessage{Message: m, finish: finish}
}

View File

@ -0,0 +1,12 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package format formats structured events.
The "application/cloudevents+json" format is built-in and always
available. Other formats may be added.
*/
package format

View File

@ -0,0 +1,83 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package format
import (
"encoding/json"
"fmt"
"strings"
"github.com/cloudevents/sdk-go/v2/event"
)
// Format marshals and unmarshals structured events to bytes.
type Format interface {
// MediaType identifies the format
MediaType() string
// Marshal event to bytes
Marshal(*event.Event) ([]byte, error)
// Unmarshal bytes to event
Unmarshal([]byte, *event.Event) error
}
// Prefix for event-format media types.
const Prefix = "application/cloudevents"
// IsFormat returns true if mediaType begins with "application/cloudevents"
func IsFormat(mediaType string) bool { return strings.HasPrefix(mediaType, Prefix) }
// JSON is the built-in "application/cloudevents+json" format.
var JSON = jsonFmt{}
type jsonFmt struct{}
func (jsonFmt) MediaType() string { return event.ApplicationCloudEventsJSON }
func (jsonFmt) Marshal(e *event.Event) ([]byte, error) { return json.Marshal(e) }
func (jsonFmt) Unmarshal(b []byte, e *event.Event) error {
return json.Unmarshal(b, e)
}
// built-in formats
var formats map[string]Format
func init() {
formats = map[string]Format{}
Add(JSON)
}
// Lookup returns the format for contentType, or nil if not found.
func Lookup(contentType string) Format {
i := strings.IndexRune(contentType, ';')
if i == -1 {
i = len(contentType)
}
contentType = strings.TrimSpace(strings.ToLower(contentType[0:i]))
return formats[contentType]
}
func unknown(mediaType string) error {
return fmt.Errorf("unknown event format media-type %#v", mediaType)
}
// Add a new Format. It can be retrieved by Lookup(f.MediaType())
func Add(f Format) { formats[f.MediaType()] = f }
// Marshal an event to bytes using the mediaType event format.
func Marshal(mediaType string, e *event.Event) ([]byte, error) {
if f := formats[mediaType]; f != nil {
return f.Marshal(e)
}
return nil, unknown(mediaType)
}
// Unmarshal bytes to an event using the mediaType event format.
func Unmarshal(mediaType string, b []byte, e *event.Event) error {
if f := formats[mediaType]; f != nil {
return f.Unmarshal(b, e)
}
return unknown(mediaType)
}

View File

@ -0,0 +1,153 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"context"
"github.com/cloudevents/sdk-go/v2/binding/spec"
)
// MessageReader defines the read-related portion of the Message interface.
//
// The ReadStructured and ReadBinary methods allows to perform an optimized encoding of a Message to a specific data structure.
//
// If MessageReader.ReadEncoding() can be equal to EncodingBinary, then the implementation of MessageReader
// MUST also implement MessageMetadataReader.
//
// A Sender should try each method of interest and fall back to binding.ToEvent() if none are supported.
// An out of the box algorithm is provided for writing a message: binding.Write().
type MessageReader interface {
// Return the type of the message Encoding.
// The encoding should be preferably computed when the message is constructed.
ReadEncoding() Encoding
// ReadStructured transfers a structured-mode event to a StructuredWriter.
// It must return ErrNotStructured if message is not in structured mode.
//
// Returns a different err if something wrong happened while trying to read the structured event.
// In this case, the caller must Finish the message with appropriate error.
//
// This allows Senders to avoid re-encoding messages that are
// already in suitable structured form.
ReadStructured(context.Context, StructuredWriter) error
// ReadBinary transfers a binary-mode event to an BinaryWriter.
// It must return ErrNotBinary if message is not in binary mode.
//
// The implementation of ReadBinary must not control the lifecycle with BinaryWriter.Start() and BinaryWriter.End(),
// because the caller must control the lifecycle.
//
// Returns a different err if something wrong happened while trying to read the binary event
// In this case, the caller must Finish the message with appropriate error
//
// This allows Senders to avoid re-encoding messages that are
// already in suitable binary form.
ReadBinary(context.Context, BinaryWriter) error
}
// MessageMetadataReader defines how to read metadata from a binary/event message
//
// If a message implementing MessageReader is encoded as binary (MessageReader.ReadEncoding() == EncodingBinary)
// or it's an EventMessage, then it's safe to assume that it also implements this interface
type MessageMetadataReader interface {
// GetAttribute returns:
//
// * attribute, value: if the message contains an attribute of that attribute kind
// * attribute, nil: if the message spec version supports the attribute kind, but doesn't have any value
// * nil, nil: if the message spec version doesn't support the attribute kind
GetAttribute(attributeKind spec.Kind) (spec.Attribute, interface{})
// GetExtension returns the value of that extension, if any.
GetExtension(name string) interface{}
}
// Message is the interface to a binding-specific message containing an event.
//
// Reliable Delivery
//
// There are 3 reliable qualities of service for messages:
//
// 0/at-most-once/unreliable: messages can be dropped silently.
//
// 1/at-least-once: messages are not dropped without signaling an error
// to the sender, but they may be duplicated in the event of a re-send.
//
// 2/exactly-once: messages are never dropped (without error) or
// duplicated, as long as both sending and receiving ends maintain
// some binding-specific delivery state. Whether this is persisted
// depends on the configuration of the binding implementations.
//
// The Message interface supports QoS 0 and 1, the ExactlyOnceMessage interface
// supports QoS 2
//
// Message includes the MessageReader interface to read messages. Every binding.Message implementation *must* specify if the message can be accessed one or more times.
//
// When a Message can be forgotten by the entity who produced the message, Message.Finish() *must* be invoked.
type Message interface {
MessageReader
// Finish *must* be called when message from a Receiver can be forgotten by
// the receiver. A QoS 1 sender should not call Finish() until it gets an acknowledgment of
// receipt on the underlying transport. For QoS 2 see ExactlyOnceMessage.
//
// Note that, depending on the Message implementation, forgetting to Finish the message
// could produce memory/resources leaks!
//
// Passing a non-nil err indicates sending or processing failed.
// A non-nil return indicates that the message was not accepted
// by the receivers peer.
Finish(error) error
}
// ExactlyOnceMessage is implemented by received Messages
// that support QoS 2. Only transports that support QoS 2 need to
// implement or use this interface.
type ExactlyOnceMessage interface {
Message
// Received is called by a forwarding QoS2 Sender when it gets
// acknowledgment of receipt (e.g. AMQP 'accept' or MQTT PUBREC)
//
// The receiver must call settle(nil) when it get's the ack-of-ack
// (e.g. AMQP 'settle' or MQTT PUBCOMP) or settle(err) if the
// transfer fails.
//
// Finally the Sender calls Finish() to indicate the message can be
// discarded.
//
// If sending fails, or if the sender does not support QoS 2, then
// Finish() may be called without any call to Received()
Received(settle func(error))
}
// MessageContext interface exposes the internal context that a message might contain
// Only some Message implementations implement this interface.
type MessageContext interface {
// Get the context associated with this message
Context() context.Context
}
// MessageWrapper interface is used to walk through a decorated Message and unwrap it.
type MessageWrapper interface {
Message
MessageMetadataReader
// Method to get the wrapped message
GetWrappedMessage() Message
}
func UnwrapMessage(message Message) Message {
m := message
for m != nil {
switch mt := m.(type) {
case MessageWrapper:
m = mt.GetWrappedMessage()
default:
return m
}
}
return m
}

View File

@ -0,0 +1,141 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package spec
import (
"fmt"
"time"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/types"
)
// Kind is a version-independent identifier for a CloudEvent context attribute.
type Kind uint8
const (
// Required cloudevents attributes
ID Kind = iota
Source
SpecVersion
Type
// Optional cloudevents attributes
DataContentType
DataSchema
Subject
Time
)
const nAttrs = int(Time) + 1
var kindNames = [nAttrs]string{
"id",
"source",
"specversion",
"type",
"datacontenttype",
"dataschema",
"subject",
"time",
}
// String is a human-readable string, for a valid attribute name use Attribute.Name
func (k Kind) String() string { return kindNames[k] }
// IsRequired returns true for attributes defined as "required" by the CE spec.
func (k Kind) IsRequired() bool { return k < DataContentType }
// Attribute is a named attribute accessor.
// The attribute name is specific to a Version.
type Attribute interface {
Kind() Kind
// Name of the attribute with respect to the current spec Version() with prefix
PrefixedName() string
// Name of the attribute with respect to the current spec Version()
Name() string
// Version of the spec that this attribute belongs to
Version() Version
// Get the value of this attribute from an event context
Get(event.EventContextReader) interface{}
// Set the value of this attribute on an event context
Set(event.EventContextWriter, interface{}) error
// Delete this attribute from and event context, when possible
Delete(event.EventContextWriter) error
}
// accessor provides Kind, Get, Set.
type accessor interface {
Kind() Kind
Get(event.EventContextReader) interface{}
Set(event.EventContextWriter, interface{}) error
Delete(event.EventContextWriter) error
}
var acc = [nAttrs]accessor{
&aStr{aKind(ID), event.EventContextReader.GetID, event.EventContextWriter.SetID},
&aStr{aKind(Source), event.EventContextReader.GetSource, event.EventContextWriter.SetSource},
&aStr{aKind(SpecVersion), event.EventContextReader.GetSpecVersion, func(writer event.EventContextWriter, s string) error { return nil }},
&aStr{aKind(Type), event.EventContextReader.GetType, event.EventContextWriter.SetType},
&aStr{aKind(DataContentType), event.EventContextReader.GetDataContentType, event.EventContextWriter.SetDataContentType},
&aStr{aKind(DataSchema), event.EventContextReader.GetDataSchema, event.EventContextWriter.SetDataSchema},
&aStr{aKind(Subject), event.EventContextReader.GetSubject, event.EventContextWriter.SetSubject},
&aTime{aKind(Time), event.EventContextReader.GetTime, event.EventContextWriter.SetTime},
}
// aKind implements Kind()
type aKind Kind
func (kind aKind) Kind() Kind { return Kind(kind) }
type aStr struct {
aKind
get func(event.EventContextReader) string
set func(event.EventContextWriter, string) error
}
func (a *aStr) Get(c event.EventContextReader) interface{} {
if s := a.get(c); s != "" {
return s
}
return nil // Treat blank as missing
}
func (a *aStr) Set(c event.EventContextWriter, v interface{}) error {
s, err := types.ToString(v)
if err != nil {
return fmt.Errorf("invalid value for %s: %#v", a.Kind(), v)
}
return a.set(c, s)
}
func (a *aStr) Delete(c event.EventContextWriter) error {
return a.set(c, "")
}
type aTime struct {
aKind
get func(event.EventContextReader) time.Time
set func(event.EventContextWriter, time.Time) error
}
func (a *aTime) Get(c event.EventContextReader) interface{} {
if v := a.get(c); !v.IsZero() {
return v
}
return nil // Treat zero time as missing.
}
func (a *aTime) Set(c event.EventContextWriter, v interface{}) error {
t, err := types.ToTime(v)
if err != nil {
return fmt.Errorf("invalid value for %s: %#v", a.Kind(), v)
}
return a.set(c, t)
}
func (a *aTime) Delete(c event.EventContextWriter) error {
return a.set(c, time.Time{})
}

View File

@ -0,0 +1,13 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package spec provides spec-version metadata.
For use by code that maps events using (prefixed) attribute name strings.
Supports handling multiple spec versions uniformly.
*/
package spec

View File

@ -0,0 +1,81 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package spec
import (
"github.com/cloudevents/sdk-go/v2/event"
)
type matchExactVersion struct {
version
}
func (v *matchExactVersion) Attribute(name string) Attribute { return v.attrMap[name] }
var _ Version = (*matchExactVersion)(nil)
func newMatchExactVersionVersion(
prefix string,
attributeNameMatchMapper func(string) string,
context event.EventContext,
convert func(event.EventContextConverter) event.EventContext,
attrs ...*attribute,
) *matchExactVersion {
v := &matchExactVersion{
version: version{
prefix: prefix,
context: context,
convert: convert,
attrMap: map[string]Attribute{},
attrs: make([]Attribute, len(attrs)),
},
}
for i, a := range attrs {
a.version = v
v.attrs[i] = a
v.attrMap[attributeNameMatchMapper(a.name)] = a
}
return v
}
// WithPrefixMatchExact returns a set of versions with prefix added to all attribute names.
func WithPrefixMatchExact(attributeNameMatchMapper func(string) string, prefix string) *Versions {
attr := func(name string, kind Kind) *attribute {
return &attribute{accessor: acc[kind], name: name}
}
vs := &Versions{
m: map[string]Version{},
prefix: prefix,
all: []Version{
newMatchExactVersionVersion(prefix, attributeNameMatchMapper, event.EventContextV1{}.AsV1(),
func(c event.EventContextConverter) event.EventContext { return c.AsV1() },
attr("id", ID),
attr("source", Source),
attr("specversion", SpecVersion),
attr("type", Type),
attr("datacontenttype", DataContentType),
attr("dataschema", DataSchema),
attr("subject", Subject),
attr("time", Time),
),
newMatchExactVersionVersion(prefix, attributeNameMatchMapper, event.EventContextV03{}.AsV03(),
func(c event.EventContextConverter) event.EventContext { return c.AsV03() },
attr("specversion", SpecVersion),
attr("type", Type),
attr("source", Source),
attr("schemaurl", DataSchema),
attr("subject", Subject),
attr("id", ID),
attr("time", Time),
attr("datacontenttype", DataContentType),
),
},
}
for _, v := range vs.all {
vs.m[v.String()] = v
}
return vs
}

View File

@ -0,0 +1,189 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package spec
import (
"strings"
"github.com/cloudevents/sdk-go/v2/event"
)
// Version provides meta-data for a single spec-version.
type Version interface {
// String name of the version, e.g. "1.0"
String() string
// Prefix for attribute names.
Prefix() string
// Attribute looks up a prefixed attribute name (case insensitive).
// Returns nil if not found.
Attribute(prefixedName string) Attribute
// Attribute looks up the attribute from kind.
// Returns nil if not found.
AttributeFromKind(kind Kind) Attribute
// Attributes returns all the context attributes for this version.
Attributes() []Attribute
// Convert translates a context to this version.
Convert(event.EventContextConverter) event.EventContext
// NewContext returns a new context for this version.
NewContext() event.EventContext
// SetAttribute sets named attribute to value.
//
// Name is case insensitive.
// Does nothing if name does not start with prefix.
SetAttribute(context event.EventContextWriter, name string, value interface{}) error
}
// Versions contains all known versions with the same attribute prefix.
type Versions struct {
prefix string
all []Version
m map[string]Version
}
// Versions returns the list of all known versions, most recent first.
func (vs *Versions) Versions() []Version { return vs.all }
// Version returns the named version.
func (vs *Versions) Version(name string) Version {
return vs.m[name]
}
// Latest returns the latest Version
func (vs *Versions) Latest() Version { return vs.all[0] }
// PrefixedSpecVersionName returns the specversion attribute PrefixedName
func (vs *Versions) PrefixedSpecVersionName() string { return vs.prefix + "specversion" }
// Prefix is the lowercase attribute name prefix.
func (vs *Versions) Prefix() string { return vs.prefix }
type attribute struct {
accessor
name string
version Version
}
func (a *attribute) PrefixedName() string { return a.version.Prefix() + a.name }
func (a *attribute) Name() string { return a.name }
func (a *attribute) Version() Version { return a.version }
type version struct {
prefix string
context event.EventContext
convert func(event.EventContextConverter) event.EventContext
attrMap map[string]Attribute
attrs []Attribute
}
func (v *version) Attribute(name string) Attribute { return v.attrMap[strings.ToLower(name)] }
func (v *version) Attributes() []Attribute { return v.attrs }
func (v *version) String() string { return v.context.GetSpecVersion() }
func (v *version) Prefix() string { return v.prefix }
func (v *version) NewContext() event.EventContext { return v.context.Clone() }
// HasPrefix is a case-insensitive prefix check.
func (v *version) HasPrefix(name string) bool {
return strings.HasPrefix(strings.ToLower(name), v.prefix)
}
func (v *version) Convert(c event.EventContextConverter) event.EventContext { return v.convert(c) }
func (v *version) SetAttribute(c event.EventContextWriter, name string, value interface{}) error {
if a := v.Attribute(name); a != nil { // Standard attribute
return a.Set(c, value)
}
name = strings.ToLower(name)
var err error
if v.HasPrefix(name) { // Extension attribute
return c.SetExtension(strings.TrimPrefix(name, v.prefix), value)
}
return err
}
func (v *version) AttributeFromKind(kind Kind) Attribute {
for _, a := range v.Attributes() {
if a.Kind() == kind {
return a
}
}
return nil
}
func newVersion(
prefix string,
context event.EventContext,
convert func(event.EventContextConverter) event.EventContext,
attrs ...*attribute,
) *version {
v := &version{
prefix: strings.ToLower(prefix),
context: context,
convert: convert,
attrMap: map[string]Attribute{},
attrs: make([]Attribute, len(attrs)),
}
for i, a := range attrs {
a.version = v
v.attrs[i] = a
v.attrMap[strings.ToLower(a.PrefixedName())] = a
}
return v
}
// WithPrefix returns a set of versions with prefix added to all attribute names.
func WithPrefix(prefix string) *Versions {
attr := func(name string, kind Kind) *attribute {
return &attribute{accessor: acc[kind], name: name}
}
vs := &Versions{
m: map[string]Version{},
prefix: prefix,
all: []Version{
newVersion(prefix, event.EventContextV1{}.AsV1(),
func(c event.EventContextConverter) event.EventContext { return c.AsV1() },
attr("id", ID),
attr("source", Source),
attr("specversion", SpecVersion),
attr("type", Type),
attr("datacontenttype", DataContentType),
attr("dataschema", DataSchema),
attr("subject", Subject),
attr("time", Time),
),
newVersion(prefix, event.EventContextV03{}.AsV03(),
func(c event.EventContextConverter) event.EventContext { return c.AsV03() },
attr("specversion", SpecVersion),
attr("type", Type),
attr("source", Source),
attr("schemaurl", DataSchema),
attr("subject", Subject),
attr("id", ID),
attr("time", Time),
attr("datacontenttype", DataContentType),
),
},
}
for _, v := range vs.all {
vs.m[v.String()] = v
}
return vs
}
// New returns a set of versions
func New() *Versions { return WithPrefix("") }
// Built-in un-prefixed versions.
var (
VS *Versions
V03 Version
V1 Version
)
func init() {
VS = New()
V03 = VS.Version("0.3")
V1 = VS.Version("1.0")
}

View File

@ -0,0 +1,22 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"context"
"io"
"github.com/cloudevents/sdk-go/v2/binding/format"
)
// StructuredWriter is used to visit a structured Message and generate a new representation.
//
// Protocols that supports structured encoding should implement this interface to implement direct
// structured to structured encoding and event to structured encoding.
type StructuredWriter interface {
// Event receives an io.Reader for the whole event.
SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error
}

View File

@ -0,0 +1,134 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"github.com/cloudevents/sdk-go/v2/binding/format"
"github.com/cloudevents/sdk-go/v2/binding/spec"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/types"
)
// ErrCannotConvertToEvent is a generic error when a conversion of a Message to an Event fails
var ErrCannotConvertToEvent = errors.New("cannot convert message to event")
// ToEvent translates a Message with a valid Structured or Binary representation to an Event.
// This function returns the Event generated from the Message and the original encoding of the message or
// an error that points the conversion error.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
func ToEvent(ctx context.Context, message MessageReader, transformers ...Transformer) (*event.Event, error) {
if message == nil {
return nil, nil
}
messageEncoding := message.ReadEncoding()
if messageEncoding == EncodingEvent {
m := message
for m != nil {
switch mt := m.(type) {
case *EventMessage:
e := (*event.Event)(mt)
return e, Transformers(transformers).Transform(mt, (*messageToEventBuilder)(e))
case MessageWrapper:
m = mt.GetWrappedMessage()
default:
break
}
}
return nil, ErrCannotConvertToEvent
}
e := event.New()
encoder := (*messageToEventBuilder)(&e)
_, err := DirectWrite(
context.Background(),
message,
encoder,
encoder,
)
if err != nil {
return nil, err
}
return &e, Transformers(transformers).Transform((*EventMessage)(&e), encoder)
}
type messageToEventBuilder event.Event
var _ StructuredWriter = (*messageToEventBuilder)(nil)
var _ BinaryWriter = (*messageToEventBuilder)(nil)
func (b *messageToEventBuilder) SetStructuredEvent(ctx context.Context, format format.Format, ev io.Reader) error {
var buf bytes.Buffer
_, err := io.Copy(&buf, ev)
if err != nil {
return err
}
return format.Unmarshal(buf.Bytes(), (*event.Event)(b))
}
func (b *messageToEventBuilder) Start(ctx context.Context) error {
return nil
}
func (b *messageToEventBuilder) End(ctx context.Context) error {
return nil
}
func (b *messageToEventBuilder) SetData(data io.Reader) error {
buf, ok := data.(*bytes.Buffer)
if !ok {
buf = new(bytes.Buffer)
_, err := io.Copy(buf, data)
if err != nil {
return err
}
}
if buf.Len() > 0 {
b.DataEncoded = buf.Bytes()
}
return nil
}
func (b *messageToEventBuilder) SetAttribute(attribute spec.Attribute, value interface{}) error {
if value == nil {
_ = attribute.Delete(b.Context)
return nil
}
// If spec version we need to change to right context struct
if attribute.Kind() == spec.SpecVersion {
str, err := types.ToString(value)
if err != nil {
return err
}
switch str {
case event.CloudEventsVersionV03:
b.Context = b.Context.AsV03()
case event.CloudEventsVersionV1:
b.Context = b.Context.AsV1()
default:
return fmt.Errorf("unrecognized event version %s", str)
}
return nil
}
return attribute.Set(b.Context, value)
}
func (b *messageToEventBuilder) SetExtension(name string, value interface{}) error {
if value == nil {
return b.Context.SetExtension(name, nil)
}
value, err := types.Validate(value)
if err != nil {
return err
}
return b.Context.SetExtension(name, value)
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
// Transformer is an interface that implements a transformation
// process while transferring the event from the Message
// implementation to the provided encoder
//
// When a write function (binding.Write, binding.ToEvent, buffering.CopyMessage, etc.)
// takes Transformer(s) as parameter, it eventually converts the message to a form
// which correctly implements MessageMetadataReader, in order to guarantee that transformation
// is applied
type Transformer interface {
Transform(MessageMetadataReader, MessageMetadataWriter) error
}
// TransformerFunc is a type alias to implement a Transformer through a function pointer
type TransformerFunc func(MessageMetadataReader, MessageMetadataWriter) error
func (t TransformerFunc) Transform(r MessageMetadataReader, w MessageMetadataWriter) error {
return t(r, w)
}
var _ Transformer = (TransformerFunc)(nil)
// Transformers is a utility alias to run several Transformer
type Transformers []Transformer
func (t Transformers) Transform(r MessageMetadataReader, w MessageMetadataWriter) error {
for _, transformer := range t {
err := transformer.Transform(r, w)
if err != nil {
return err
}
}
return nil
}
var _ Transformer = (Transformers)(nil)

View File

@ -0,0 +1,179 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package binding
import (
"context"
"github.com/cloudevents/sdk-go/v2/event"
)
type eventEncodingKey int
const (
skipDirectStructuredEncoding eventEncodingKey = iota
skipDirectBinaryEncoding
preferredEventEncoding
)
// DirectWrite invokes the encoders. structuredWriter and binaryWriter could be nil if the protocol doesn't support it.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
// This function MUST be invoked only if message.ReadEncoding() == EncodingBinary or message.ReadEncoding() == EncodingStructured
//
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingStructured, err if message was structured but error happened during the encoding
// * EncodingBinary, err if message was binary but error happened during the encoding
// * EncodingUnknown, ErrUnknownEncoding if message is not a structured or a binary Message
func DirectWrite(
ctx context.Context,
message MessageReader,
structuredWriter StructuredWriter,
binaryWriter BinaryWriter,
transformers ...Transformer,
) (Encoding, error) {
if structuredWriter != nil && len(transformers) == 0 && !GetOrDefaultFromCtx(ctx, skipDirectStructuredEncoding, false).(bool) {
if err := message.ReadStructured(ctx, structuredWriter); err == nil {
return EncodingStructured, nil
} else if err != ErrNotStructured {
return EncodingStructured, err
}
}
if binaryWriter != nil && !GetOrDefaultFromCtx(ctx, skipDirectBinaryEncoding, false).(bool) && message.ReadEncoding() == EncodingBinary {
return EncodingBinary, writeBinaryWithTransformer(ctx, message, binaryWriter, transformers)
}
return EncodingUnknown, ErrUnknownEncoding
}
// Write executes the full algorithm to encode a Message using transformers:
// 1. It first tries direct encoding using DirectWrite
// 2. If no direct encoding is possible, it uses ToEvent to generate an Event representation
// 3. From the Event, the message is encoded back to the provided structured or binary encoders
// You can tweak the encoding process using the context decorators WithForceStructured, WithForceStructured, etc.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingUnknown, ErrUnknownEncoding if message.ReadEncoding() == EncodingUnknown
// * _, err if error happened during the encoding
func Write(
ctx context.Context,
message MessageReader,
structuredWriter StructuredWriter,
binaryWriter BinaryWriter,
transformers ...Transformer,
) (Encoding, error) {
enc := message.ReadEncoding()
var err error
// Skip direct encoding if the event is an event message
if enc != EncodingEvent {
enc, err = DirectWrite(ctx, message, structuredWriter, binaryWriter, transformers...)
if enc != EncodingUnknown {
// Message directly encoded, nothing else to do here
return enc, err
}
}
var e *event.Event
e, err = ToEvent(ctx, message, transformers...)
if err != nil {
return enc, err
}
message = (*EventMessage)(e)
if GetOrDefaultFromCtx(ctx, preferredEventEncoding, EncodingBinary).(Encoding) == EncodingStructured {
if structuredWriter != nil {
return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
}
if binaryWriter != nil {
return EncodingBinary, writeBinary(ctx, message, binaryWriter)
}
} else {
if binaryWriter != nil {
return EncodingBinary, writeBinary(ctx, message, binaryWriter)
}
if structuredWriter != nil {
return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
}
}
return EncodingUnknown, ErrUnknownEncoding
}
// WithSkipDirectStructuredEncoding skips direct structured to structured encoding during the encoding process
func WithSkipDirectStructuredEncoding(ctx context.Context, skip bool) context.Context {
return context.WithValue(ctx, skipDirectStructuredEncoding, skip)
}
// WithSkipDirectBinaryEncoding skips direct binary to binary encoding during the encoding process
func WithSkipDirectBinaryEncoding(ctx context.Context, skip bool) context.Context {
return context.WithValue(ctx, skipDirectBinaryEncoding, skip)
}
// WithPreferredEventEncoding defines the preferred encoding from event to message during the encoding process
func WithPreferredEventEncoding(ctx context.Context, enc Encoding) context.Context {
return context.WithValue(ctx, preferredEventEncoding, enc)
}
// WithForceStructured forces structured encoding during the encoding process
func WithForceStructured(ctx context.Context) context.Context {
return context.WithValue(context.WithValue(ctx, preferredEventEncoding, EncodingStructured), skipDirectBinaryEncoding, true)
}
// WithForceBinary forces binary encoding during the encoding process
func WithForceBinary(ctx context.Context) context.Context {
return context.WithValue(context.WithValue(ctx, preferredEventEncoding, EncodingBinary), skipDirectStructuredEncoding, true)
}
// GetOrDefaultFromCtx gets a configuration value from the provided context
func GetOrDefaultFromCtx(ctx context.Context, key interface{}, def interface{}) interface{} {
if val := ctx.Value(key); val != nil {
return val
} else {
return def
}
}
func writeBinaryWithTransformer(
ctx context.Context,
message MessageReader,
binaryWriter BinaryWriter,
transformers Transformers,
) error {
err := binaryWriter.Start(ctx)
if err != nil {
return err
}
err = message.ReadBinary(ctx, binaryWriter)
if err != nil {
return err
}
err = transformers.Transform(message.(MessageMetadataReader), binaryWriter)
if err != nil {
return err
}
return binaryWriter.End(ctx)
}
func writeBinary(
ctx context.Context,
message MessageReader,
binaryWriter BinaryWriter,
) error {
err := binaryWriter.Start(ctx)
if err != nil {
return err
}
err = message.ReadBinary(ctx, binaryWriter)
if err != nil {
return err
}
return binaryWriter.End(ctx)
}

View File

@ -0,0 +1,288 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"errors"
"fmt"
"io"
"runtime"
"sync"
"go.uber.org/zap"
"github.com/cloudevents/sdk-go/v2/binding"
cecontext "github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
)
// Client interface defines the runtime contract the CloudEvents client supports.
type Client interface {
// Send will transmit the given event over the client's configured transport.
Send(ctx context.Context, event event.Event) protocol.Result
// Request will transmit the given event over the client's configured
// transport and return any response event.
Request(ctx context.Context, event event.Event) (*event.Event, protocol.Result)
// StartReceiver will register the provided function for callback on receipt
// of a cloudevent. It will also start the underlying protocol as it has
// been configured.
// This call is blocking.
// Valid fn signatures are:
// * func()
// * func() error
// * func(context.Context)
// * func(context.Context) protocol.Result
// * func(event.Event)
// * func(event.Event) protocol.Result
// * func(context.Context, event.Event)
// * func(context.Context, event.Event) protocol.Result
// * func(event.Event) *event.Event
// * func(event.Event) (*event.Event, protocol.Result)
// * func(context.Context, event.Event) *event.Event
// * func(context.Context, event.Event) (*event.Event, protocol.Result)
StartReceiver(ctx context.Context, fn interface{}) error
}
// New produces a new client with the provided transport object and applied
// client options.
func New(obj interface{}, opts ...Option) (Client, error) {
c := &ceClient{
// Running runtime.GOMAXPROCS(0) doesn't update the value, just returns the current one
pollGoroutines: runtime.GOMAXPROCS(0),
observabilityService: noopObservabilityService{},
}
if p, ok := obj.(protocol.Sender); ok {
c.sender = p
}
if p, ok := obj.(protocol.Requester); ok {
c.requester = p
}
if p, ok := obj.(protocol.Responder); ok {
c.responder = p
}
if p, ok := obj.(protocol.Receiver); ok {
c.receiver = p
}
if p, ok := obj.(protocol.Opener); ok {
c.opener = p
}
if err := c.applyOptions(opts...); err != nil {
return nil, err
}
return c, nil
}
type ceClient struct {
sender protocol.Sender
requester protocol.Requester
receiver protocol.Receiver
responder protocol.Responder
// Optional.
opener protocol.Opener
observabilityService ObservabilityService
inboundContextDecorators []func(context.Context, binding.Message) context.Context
outboundContextDecorators []func(context.Context) context.Context
invoker Invoker
receiverMu sync.Mutex
eventDefaulterFns []EventDefaulter
pollGoroutines int
blockingCallback bool
}
func (c *ceClient) applyOptions(opts ...Option) error {
for _, fn := range opts {
if err := fn(c); err != nil {
return err
}
}
return nil
}
func (c *ceClient) Send(ctx context.Context, e event.Event) protocol.Result {
var err error
if c.sender == nil {
err = errors.New("sender not set")
return err
}
for _, f := range c.outboundContextDecorators {
ctx = f(ctx)
}
if len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
e = fn(ctx, e)
}
}
if err = e.Validate(); err != nil {
return err
}
// Event has been defaulted and validated, record we are going to perform send.
ctx, cb := c.observabilityService.RecordSendingEvent(ctx, e)
err = c.sender.Send(ctx, (*binding.EventMessage)(&e))
defer cb(err)
return err
}
func (c *ceClient) Request(ctx context.Context, e event.Event) (*event.Event, protocol.Result) {
var resp *event.Event
var err error
if c.requester == nil {
err = errors.New("requester not set")
return nil, err
}
for _, f := range c.outboundContextDecorators {
ctx = f(ctx)
}
if len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
e = fn(ctx, e)
}
}
if err = e.Validate(); err != nil {
return nil, err
}
// Event has been defaulted and validated, record we are going to perform request.
ctx, cb := c.observabilityService.RecordRequestEvent(ctx, e)
// If provided a requester, use it to do request/response.
var msg binding.Message
msg, err = c.requester.Request(ctx, (*binding.EventMessage)(&e))
if msg != nil {
defer func() {
if err := msg.Finish(err); err != nil {
cecontext.LoggerFrom(ctx).Warnw("failed calling message.Finish", zap.Error(err))
}
}()
}
if protocol.IsUndelivered(err) {
return nil, err
}
// try to turn msg into an event, it might not work and that is ok.
if rs, rserr := binding.ToEvent(ctx, msg); rserr != nil {
cecontext.LoggerFrom(ctx).Debugw("response: failed calling ToEvent", zap.Error(rserr), zap.Any("resp", msg))
// If the protocol returns no error, it is an ACK on the request, but we had
// issues turning the response into an event, so make an ACK Result and pass
// down the ToEvent error as well.
err = protocol.NewReceipt(true, "failed to convert response into event: %v\n%w", rserr, err)
} else {
resp = rs
}
defer cb(err, resp)
return resp, err
}
// StartReceiver sets up the given fn to handle Receive.
// See Client.StartReceiver for details. This is a blocking call.
func (c *ceClient) StartReceiver(ctx context.Context, fn interface{}) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
c.receiverMu.Lock()
defer c.receiverMu.Unlock()
if c.invoker != nil {
return fmt.Errorf("client already has a receiver")
}
invoker, err := newReceiveInvoker(fn, c.observabilityService, c.inboundContextDecorators, c.eventDefaulterFns...)
if err != nil {
return err
}
if invoker.IsReceiver() && c.receiver == nil {
return fmt.Errorf("mismatched receiver callback without protocol.Receiver supported by protocol")
}
if invoker.IsResponder() && c.responder == nil {
return fmt.Errorf("mismatched receiver callback without protocol.Responder supported by protocol")
}
c.invoker = invoker
if c.responder == nil && c.receiver == nil {
return errors.New("responder nor receiver set")
}
defer func() {
c.invoker = nil
}()
// Start Polling.
wg := sync.WaitGroup{}
for i := 0; i < c.pollGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
var msg binding.Message
var respFn protocol.ResponseFn
var err error
if c.responder != nil {
msg, respFn, err = c.responder.Respond(ctx)
} else if c.receiver != nil {
msg, err = c.receiver.Receive(ctx)
respFn = noRespFn
}
if err == io.EOF { // Normal close
return
}
if err != nil {
cecontext.LoggerFrom(ctx).Warn("Error while receiving a message: ", err)
continue
}
callback := func() {
if err := c.invoker.Invoke(ctx, msg, respFn); err != nil {
cecontext.LoggerFrom(ctx).Warn("Error while handling a message: ", err)
}
}
if c.blockingCallback {
callback()
} else {
// Do not block on the invoker.
wg.Add(1)
go func() {
defer wg.Done()
callback()
}()
}
}
}()
}
// Start the opener, if set.
if c.opener != nil {
if err = c.opener.OpenInbound(ctx); err != nil {
err = fmt.Errorf("error while opening the inbound connection: %w", err)
cancel()
}
}
wg.Wait()
return err
}
// noRespFn is used to simply forward the protocol.Result for receivers that aren't responders
func noRespFn(_ context.Context, _ binding.Message, r protocol.Result, _ ...binding.Transformer) error {
return r
}

View File

@ -0,0 +1,35 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"github.com/cloudevents/sdk-go/v2/protocol/http"
)
// NewHTTP provides the good defaults for the common case using an HTTP
// Protocol client.
// The WithTimeNow, and WithUUIDs client options are also applied to the
// client, all outbound events will have a time and id set if not already
// present.
func NewHTTP(opts ...http.Option) (Client, error) {
p, err := http.New(opts...)
if err != nil {
return nil, err
}
c, err := New(p, WithTimeNow(), WithUUIDs())
if err != nil {
return nil, err
}
return c, nil
}
// NewDefault has been replaced by NewHTTP
// Deprecated. To get the same as NewDefault provided, please use NewHTTP with
// the observability service passed as an option, or client.NewClientHTTP from
// package github.com/cloudevents/sdk-go/observability/opencensus/v2/client
var NewDefault = NewHTTP

View File

@ -0,0 +1,12 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
// NewObserved produces a new client with the provided transport object and applied
// client options.
// Deprecated: This now has the same behaviour of New, and will be removed in future releases.
// As New, you must provide the observability service to use.
var NewObserved = New

View File

@ -0,0 +1,57 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"time"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/google/uuid"
)
// EventDefaulter is the function signature for extensions that are able
// to perform event defaulting.
type EventDefaulter func(ctx context.Context, event event.Event) event.Event
// DefaultIDToUUIDIfNotSet will inspect the provided event and assign a UUID to
// context.ID if it is found to be empty.
func DefaultIDToUUIDIfNotSet(ctx context.Context, event event.Event) event.Event {
if event.Context != nil {
if event.ID() == "" {
event.Context = event.Context.Clone()
event.SetID(uuid.New().String())
}
}
return event
}
// DefaultTimeToNowIfNotSet will inspect the provided event and assign a new
// Timestamp to context.Time if it is found to be nil or zero.
func DefaultTimeToNowIfNotSet(ctx context.Context, event event.Event) event.Event {
if event.Context != nil {
if event.Time().IsZero() {
event.Context = event.Context.Clone()
event.SetTime(time.Now())
}
}
return event
}
// NewDefaultDataContentTypeIfNotSet returns a defaulter that will inspect the
// provided event and set the provided content type if content type is found
// to be empty.
func NewDefaultDataContentTypeIfNotSet(contentType string) EventDefaulter {
return func(ctx context.Context, event event.Event) event.Event {
if event.Context != nil {
if event.DataContentType() == "" {
event.SetDataContentType(contentType)
}
}
return event
}
}

View File

@ -0,0 +1,11 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package client holds the recommended entry points for interacting with the CloudEvents Golang SDK. The client wraps
a selected transport. The client adds validation and defaulting for sending events, and flexible receiver method
registration. For full details, read the `client.Client` documentation.
*/
package client

View File

@ -0,0 +1,45 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
cecontext "github.com/cloudevents/sdk-go/v2/context"
thttp "github.com/cloudevents/sdk-go/v2/protocol/http"
"go.uber.org/zap"
"net/http"
)
func NewHTTPReceiveHandler(ctx context.Context, p *thttp.Protocol, fn interface{}) (*EventReceiver, error) {
invoker, err := newReceiveInvoker(fn, noopObservabilityService{}, nil) //TODO(slinkydeveloper) maybe not nil?
if err != nil {
return nil, err
}
return &EventReceiver{
p: p,
invoker: invoker,
}, nil
}
type EventReceiver struct {
p *thttp.Protocol
invoker Invoker
}
func (r *EventReceiver) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Prepare to handle the message if there's one (context cancellation will ensure this closes)
go func() {
ctx := req.Context()
msg, respFn, err := r.p.Respond(ctx)
if err != nil {
cecontext.LoggerFrom(context.TODO()).Debugw("failed to call Respond", zap.Error(err))
} else if err := r.invoker.Invoke(ctx, msg, respFn); err != nil {
cecontext.LoggerFrom(context.TODO()).Debugw("failed to call Invoke", zap.Error(err))
}
}()
r.p.ServeHTTP(rw, req)
}

View File

@ -0,0 +1,137 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"fmt"
"github.com/cloudevents/sdk-go/v2/binding"
cecontext "github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
)
type Invoker interface {
Invoke(context.Context, binding.Message, protocol.ResponseFn) error
IsReceiver() bool
IsResponder() bool
}
var _ Invoker = (*receiveInvoker)(nil)
func newReceiveInvoker(fn interface{}, observabilityService ObservabilityService, inboundContextDecorators []func(context.Context, binding.Message) context.Context, fns ...EventDefaulter) (Invoker, error) {
r := &receiveInvoker{
eventDefaulterFns: fns,
observabilityService: observabilityService,
inboundContextDecorators: inboundContextDecorators,
}
if fn, err := receiver(fn); err != nil {
return nil, err
} else {
r.fn = fn
}
return r, nil
}
type receiveInvoker struct {
fn *receiverFn
observabilityService ObservabilityService
eventDefaulterFns []EventDefaulter
inboundContextDecorators []func(context.Context, binding.Message) context.Context
}
func (r *receiveInvoker) Invoke(ctx context.Context, m binding.Message, respFn protocol.ResponseFn) (err error) {
defer func() {
err = m.Finish(err)
}()
var respMsg binding.Message
var result protocol.Result
e, eventErr := binding.ToEvent(ctx, m)
switch {
case eventErr != nil && r.fn.hasEventIn:
r.observabilityService.RecordReceivedMalformedEvent(ctx, eventErr)
return respFn(ctx, nil, protocol.NewReceipt(false, "failed to convert Message to Event: %w", eventErr))
case r.fn != nil:
// Check if event is valid before invoking the receiver function
if e != nil {
if validationErr := e.Validate(); validationErr != nil {
r.observabilityService.RecordReceivedMalformedEvent(ctx, validationErr)
return respFn(ctx, nil, protocol.NewReceipt(false, "validation error in incoming event: %w", validationErr))
}
}
// Let's invoke the receiver fn
var resp *event.Event
resp, result = func() (resp *event.Event, result protocol.Result) {
defer func() {
if r := recover(); r != nil {
result = fmt.Errorf("call to Invoker.Invoke(...) has panicked: %v", r)
cecontext.LoggerFrom(ctx).Error(result)
}
}()
ctx = computeInboundContext(m, ctx, r.inboundContextDecorators)
var cb func(error)
ctx, cb = r.observabilityService.RecordCallingInvoker(ctx, e)
resp, result = r.fn.invoke(ctx, e)
defer cb(result)
return
}()
if respFn == nil {
break
}
// Apply the defaulter chain to the outgoing event.
if resp != nil && len(r.eventDefaulterFns) > 0 {
for _, fn := range r.eventDefaulterFns {
*resp = fn(ctx, *resp)
}
// Validate the event conforms to the CloudEvents Spec.
if vErr := resp.Validate(); vErr != nil {
cecontext.LoggerFrom(ctx).Errorf("cloudevent validation failed on response event: %v", vErr)
}
}
// because binding.Message is an interface, casting a nil resp
// here would make future comparisons to nil false
if resp != nil {
respMsg = (*binding.EventMessage)(resp)
}
}
if respFn == nil {
// let the protocol ACK based on the result
return result
}
return respFn(ctx, respMsg, result)
}
func (r *receiveInvoker) IsReceiver() bool {
return !r.fn.hasEventOut
}
func (r *receiveInvoker) IsResponder() bool {
return r.fn.hasEventOut
}
func computeInboundContext(message binding.Message, fallback context.Context, inboundContextDecorators []func(context.Context, binding.Message) context.Context) context.Context {
result := fallback
if mctx, ok := message.(binding.MessageContext); ok {
result = cecontext.ValuesDelegating(mctx.Context(), fallback)
}
for _, f := range inboundContextDecorators {
result = f(result, message)
}
return result
}

View File

@ -0,0 +1,54 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"github.com/cloudevents/sdk-go/v2/binding"
"github.com/cloudevents/sdk-go/v2/event"
)
// ObservabilityService is an interface users can implement to record metrics, create tracing spans, and plug other observability tools in the Client
type ObservabilityService interface {
// InboundContextDecorators is a method that returns the InboundContextDecorators that must be mounted in the Client to properly propagate some tracing informations.
InboundContextDecorators() []func(context.Context, binding.Message) context.Context
// RecordReceivedMalformedEvent is invoked when an event was received but it's malformed or invalid.
RecordReceivedMalformedEvent(ctx context.Context, err error)
// RecordCallingInvoker is invoked before the user function is invoked.
// The returned callback will be invoked after the user finishes to process the event with the eventual processing error
// The error provided to the callback could be both a processing error, or a result
RecordCallingInvoker(ctx context.Context, event *event.Event) (context.Context, func(errOrResult error))
// RecordSendingEvent is invoked before the event is sent.
// The returned callback will be invoked when the response is received
// The error provided to the callback could be both a processing error, or a result
RecordSendingEvent(ctx context.Context, event event.Event) (context.Context, func(errOrResult error))
// RecordRequestEvent is invoked before the event is requested.
// The returned callback will be invoked when the response is received
RecordRequestEvent(ctx context.Context, event event.Event) (context.Context, func(errOrResult error, event *event.Event))
}
type noopObservabilityService struct{}
func (n noopObservabilityService) InboundContextDecorators() []func(context.Context, binding.Message) context.Context {
return nil
}
func (n noopObservabilityService) RecordReceivedMalformedEvent(ctx context.Context, err error) {}
func (n noopObservabilityService) RecordCallingInvoker(ctx context.Context, event *event.Event) (context.Context, func(errOrResult error)) {
return ctx, func(errOrResult error) {}
}
func (n noopObservabilityService) RecordSendingEvent(ctx context.Context, event event.Event) (context.Context, func(errOrResult error)) {
return ctx, func(errOrResult error) {}
}
func (n noopObservabilityService) RecordRequestEvent(ctx context.Context, e event.Event) (context.Context, func(errOrResult error, event *event.Event)) {
return ctx, func(errOrResult error, event *event.Event) {}
}

View File

@ -0,0 +1,128 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"fmt"
"github.com/cloudevents/sdk-go/v2/binding"
)
// Option is the function signature required to be considered an client.Option.
type Option func(interface{}) error
// WithEventDefaulter adds an event defaulter to the end of the defaulter chain.
func WithEventDefaulter(fn EventDefaulter) Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
if fn == nil {
return fmt.Errorf("client option was given an nil event defaulter")
}
c.eventDefaulterFns = append(c.eventDefaulterFns, fn)
}
return nil
}
}
func WithForceBinary() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.outboundContextDecorators = append(c.outboundContextDecorators, binding.WithForceBinary)
}
return nil
}
}
func WithForceStructured() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.outboundContextDecorators = append(c.outboundContextDecorators, binding.WithForceStructured)
}
return nil
}
}
// WithUUIDs adds DefaultIDToUUIDIfNotSet event defaulter to the end of the
// defaulter chain.
func WithUUIDs() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultIDToUUIDIfNotSet)
}
return nil
}
}
// WithTimeNow adds DefaultTimeToNowIfNotSet event defaulter to the end of the
// defaulter chain.
func WithTimeNow() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultTimeToNowIfNotSet)
}
return nil
}
}
// WithTracePropagation enables trace propagation via the distributed tracing
// extension.
// Deprecated: this is now noop and will be removed in future releases.
// Don't use distributed tracing extension to propagate traces:
// https://github.com/cloudevents/spec/blob/v1.0.1/extensions/distributed-tracing.md#using-the-distributed-tracing-extension
func WithTracePropagation() Option {
return func(i interface{}) error {
return nil
}
}
// WithPollGoroutines configures how much goroutines should be used to
// poll the Receiver/Responder/Protocol implementations.
// Default value is GOMAXPROCS
func WithPollGoroutines(pollGoroutines int) Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.pollGoroutines = pollGoroutines
}
return nil
}
}
// WithObservabilityService configures the observability service to use
// to record traces and metrics
func WithObservabilityService(service ObservabilityService) Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.observabilityService = service
c.inboundContextDecorators = append(c.inboundContextDecorators, service.InboundContextDecorators()...)
}
return nil
}
}
// WithInboundContextDecorator configures a new inbound context decorator.
// Inbound context decorators are invoked to wrap additional informations from the binding.Message
// and propagate these informations in the context passed to the event receiver.
func WithInboundContextDecorator(dec func(context.Context, binding.Message) context.Context) Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.inboundContextDecorators = append(c.inboundContextDecorators, dec)
}
return nil
}
}
// WithBlockingCallback makes the callback passed into StartReceiver is executed as a blocking call,
// i.e. in each poll go routine, the next event will not be received until the callback on current event completes.
// To make event processing serialized (no concurrency), use this option along with WithPollGoroutines(1)
func WithBlockingCallback() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.blockingCallback = true
}
return nil
}
}

View File

@ -0,0 +1,194 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package client
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
)
// ReceiveFull is the signature of a fn to be invoked for incoming cloudevents.
type ReceiveFull func(context.Context, event.Event) protocol.Result
type receiverFn struct {
numIn int
numOut int
fnValue reflect.Value
hasContextIn bool
hasEventIn bool
hasEventOut bool
hasResultOut bool
}
const (
inParamUsage = "expected a function taking either no parameters, one or more of (context.Context, event.Event) ordered"
outParamUsage = "expected a function returning one or mode of (*event.Event, protocol.Result) ordered"
)
var (
contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
eventType = reflect.TypeOf((*event.Event)(nil)).Elem()
eventPtrType = reflect.TypeOf((*event.Event)(nil)) // want the ptr type
resultType = reflect.TypeOf((*protocol.Result)(nil)).Elem()
)
// receiver creates a receiverFn wrapper class that is used by the client to
// validate and invoke the provided function.
// Valid fn signatures are:
// * func()
// * func() protocol.Result
// * func(context.Context)
// * func(context.Context) protocol.Result
// * func(event.Event)
// * func(event.Event) transport.Result
// * func(context.Context, event.Event)
// * func(context.Context, event.Event) protocol.Result
// * func(event.Event) *event.Event
// * func(event.Event) (*event.Event, protocol.Result)
// * func(context.Context, event.Event) *event.Event
// * func(context.Context, event.Event) (*event.Event, protocol.Result)
//
func receiver(fn interface{}) (*receiverFn, error) {
fnType := reflect.TypeOf(fn)
if fnType.Kind() != reflect.Func {
return nil, errors.New("must pass a function to handle events")
}
r := &receiverFn{
fnValue: reflect.ValueOf(fn),
numIn: fnType.NumIn(),
numOut: fnType.NumOut(),
}
if err := r.validate(fnType); err != nil {
return nil, err
}
return r, nil
}
func (r *receiverFn) invoke(ctx context.Context, e *event.Event) (*event.Event, protocol.Result) {
args := make([]reflect.Value, 0, r.numIn)
if r.numIn > 0 {
if r.hasContextIn {
args = append(args, reflect.ValueOf(ctx))
}
if r.hasEventIn {
args = append(args, reflect.ValueOf(*e))
}
}
v := r.fnValue.Call(args)
var respOut protocol.Result
var eOut *event.Event
if r.numOut > 0 {
i := 0
if r.hasEventOut {
if eo, ok := v[i].Interface().(*event.Event); ok {
eOut = eo
}
i++ // <-- note, need to inc i.
}
if r.hasResultOut {
if resp, ok := v[i].Interface().(protocol.Result); ok {
respOut = resp
}
}
}
return eOut, respOut
}
// Verifies that the inputs to a function have a valid signature
// Valid input is to be [0, all] of
// context.Context, event.Event in this order.
func (r *receiverFn) validateInParamSignature(fnType reflect.Type) error {
r.hasContextIn = false
r.hasEventIn = false
switch fnType.NumIn() {
case 2:
// has to be (context.Context, event.Event)
if !eventType.ConvertibleTo(fnType.In(1)) {
return fmt.Errorf("%s; cannot convert parameter 2 to %s from event.Event", inParamUsage, fnType.In(1))
} else {
r.hasEventIn = true
}
fallthrough
case 1:
if !contextType.ConvertibleTo(fnType.In(0)) {
if !eventType.ConvertibleTo(fnType.In(0)) {
return fmt.Errorf("%s; cannot convert parameter 1 to %s from context.Context or event.Event", inParamUsage, fnType.In(0))
} else if r.hasEventIn {
return fmt.Errorf("%s; duplicate parameter of type event.Event", inParamUsage)
} else {
r.hasEventIn = true
}
} else {
r.hasContextIn = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many parameters (%d)", inParamUsage, fnType.NumIn())
}
}
// Verifies that the outputs of a function have a valid signature
// Valid output signatures to be [0, all] of
// *event.Event, transport.Result in this order
func (r *receiverFn) validateOutParamSignature(fnType reflect.Type) error {
r.hasEventOut = false
r.hasResultOut = false
switch fnType.NumOut() {
case 2:
// has to be (*event.Event, transport.Result)
if !fnType.Out(1).ConvertibleTo(resultType) {
return fmt.Errorf("%s; cannot convert parameter 2 from %s to event.Response", outParamUsage, fnType.Out(1))
} else {
r.hasResultOut = true
}
fallthrough
case 1:
if !fnType.Out(0).ConvertibleTo(resultType) {
if !fnType.Out(0).ConvertibleTo(eventPtrType) {
return fmt.Errorf("%s; cannot convert parameter 1 from %s to *event.Event or transport.Result", outParamUsage, fnType.Out(0))
} else {
r.hasEventOut = true
}
} else if r.hasResultOut {
return fmt.Errorf("%s; duplicate parameter of type event.Response", outParamUsage)
} else {
r.hasResultOut = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many return types (%d)", outParamUsage, fnType.NumOut())
}
}
// validateReceiverFn validates that a function has the right number of in and
// out params and that they are of allowed types.
func (r *receiverFn) validate(fnType reflect.Type) error {
if err := r.validateInParamSignature(fnType); err != nil {
return err
}
if err := r.validateOutParamSignature(fnType); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,110 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package context
import (
"context"
"net/url"
"time"
)
// Opaque key type used to store target
type targetKeyType struct{}
var targetKey = targetKeyType{}
// WithTarget returns back a new context with the given target. Target is intended to be transport dependent.
// For http transport, `target` should be a full URL and will be injected into the outbound http request.
func WithTarget(ctx context.Context, target string) context.Context {
return context.WithValue(ctx, targetKey, target)
}
// TargetFrom looks in the given context and returns `target` as a parsed url if found and valid, otherwise nil.
func TargetFrom(ctx context.Context) *url.URL {
c := ctx.Value(targetKey)
if c != nil {
if s, ok := c.(string); ok && s != "" {
if target, err := url.Parse(s); err == nil {
return target
}
}
}
return nil
}
// Opaque key type used to store topic
type topicKeyType struct{}
var topicKey = topicKeyType{}
// WithTopic returns back a new context with the given topic. Topic is intended to be transport dependent.
// For pubsub transport, `topic` should be a Pub/Sub Topic ID.
func WithTopic(ctx context.Context, topic string) context.Context {
return context.WithValue(ctx, topicKey, topic)
}
// TopicFrom looks in the given context and returns `topic` as a string if found and valid, otherwise "".
func TopicFrom(ctx context.Context) string {
c := ctx.Value(topicKey)
if c != nil {
if s, ok := c.(string); ok {
return s
}
}
return ""
}
// Opaque key type used to store retry parameters
type retriesKeyType struct{}
var retriesKey = retriesKeyType{}
// WithRetriesConstantBackoff returns back a new context with retries parameters using constant backoff strategy.
// MaxTries is the maximum number for retries and delay is the time interval between retries
func WithRetriesConstantBackoff(ctx context.Context, delay time.Duration, maxTries int) context.Context {
return WithRetryParams(ctx, &RetryParams{
Strategy: BackoffStrategyConstant,
Period: delay,
MaxTries: maxTries,
})
}
// WithRetriesLinearBackoff returns back a new context with retries parameters using linear backoff strategy.
// MaxTries is the maximum number for retries and delay*tries is the time interval between retries
func WithRetriesLinearBackoff(ctx context.Context, delay time.Duration, maxTries int) context.Context {
return WithRetryParams(ctx, &RetryParams{
Strategy: BackoffStrategyLinear,
Period: delay,
MaxTries: maxTries,
})
}
// WithRetriesExponentialBackoff returns back a new context with retries parameters using exponential backoff strategy.
// MaxTries is the maximum number for retries and period is the amount of time to wait, used as `period * 2^retries`.
func WithRetriesExponentialBackoff(ctx context.Context, period time.Duration, maxTries int) context.Context {
return WithRetryParams(ctx, &RetryParams{
Strategy: BackoffStrategyExponential,
Period: period,
MaxTries: maxTries,
})
}
// WithRetryParams returns back a new context with retries parameters.
func WithRetryParams(ctx context.Context, rp *RetryParams) context.Context {
return context.WithValue(ctx, retriesKey, rp)
}
// RetriesFrom looks in the given context and returns the retries parameters if found.
// Otherwise returns the default retries configuration (ie. no retries).
func RetriesFrom(ctx context.Context) *RetryParams {
c := ctx.Value(retriesKey)
if c != nil {
if s, ok := c.(*RetryParams); ok {
return s
}
}
return &DefaultRetryParams
}

View File

@ -0,0 +1,25 @@
package context
import "context"
type valuesDelegating struct {
context.Context
parent context.Context
}
// ValuesDelegating wraps a child and parent context. It will perform Value()
// lookups first on the child, and then fall back to the child. All other calls
// go solely to the child context.
func ValuesDelegating(child, parent context.Context) context.Context {
return &valuesDelegating{
Context: child,
parent: parent,
}
}
func (c *valuesDelegating) Value(key interface{}) interface{} {
if val := c.Context.Value(key); val != nil {
return val
}
return c.parent.Value(key)
}

View File

@ -0,0 +1,10 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package context holds the last resort overrides and fyi objects that can be passed to clients and transports added to
context.Context objects.
*/
package context

View File

@ -0,0 +1,48 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package context
import (
"context"
"go.uber.org/zap"
)
// Opaque key type used to store logger
type loggerKeyType struct{}
var loggerKey = loggerKeyType{}
// fallbackLogger is the logger is used when there is no logger attached to the context.
var fallbackLogger *zap.SugaredLogger
func init() {
if logger, err := zap.NewProduction(); err != nil {
// We failed to create a fallback logger.
fallbackLogger = zap.NewNop().Sugar()
} else {
fallbackLogger = logger.Named("fallback").Sugar()
}
}
// WithLogger returns a new context with the logger injected into the given context.
func WithLogger(ctx context.Context, logger *zap.SugaredLogger) context.Context {
if logger == nil {
return context.WithValue(ctx, loggerKey, fallbackLogger)
}
return context.WithValue(ctx, loggerKey, logger)
}
// LoggerFrom returns the logger stored in context.
func LoggerFrom(ctx context.Context) *zap.SugaredLogger {
l := ctx.Value(loggerKey)
if l != nil {
if logger, ok := l.(*zap.SugaredLogger); ok {
return logger
}
}
return fallbackLogger
}

View File

@ -0,0 +1,76 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package context
import (
"context"
"errors"
"math"
"time"
)
type BackoffStrategy string
const (
BackoffStrategyNone = "none"
BackoffStrategyConstant = "constant"
BackoffStrategyLinear = "linear"
BackoffStrategyExponential = "exponential"
)
var DefaultRetryParams = RetryParams{Strategy: BackoffStrategyNone}
// RetryParams holds parameters applied to retries
type RetryParams struct {
// Strategy is the backoff strategy to applies between retries
Strategy BackoffStrategy
// MaxTries is the maximum number of times to retry request before giving up
MaxTries int
// Period is
// - for none strategy: no delay
// - for constant strategy: the delay interval between retries
// - for linear strategy: interval between retries = Period * retries
// - for exponential strategy: interval between retries = Period * retries^2
Period time.Duration
}
// BackoffFor tries will return the time duration that should be used for this
// current try count.
// `tries` is assumed to be the number of times the caller has already retried.
func (r *RetryParams) BackoffFor(tries int) time.Duration {
switch r.Strategy {
case BackoffStrategyConstant:
return r.Period
case BackoffStrategyLinear:
return r.Period * time.Duration(tries)
case BackoffStrategyExponential:
exp := math.Exp2(float64(tries))
return r.Period * time.Duration(exp)
case BackoffStrategyNone:
fallthrough // default
default:
return r.Period
}
}
// Backoff is a blocking call to wait for the correct amount of time for the retry.
// `tries` is assumed to be the number of times the caller has already retried.
func (r *RetryParams) Backoff(ctx context.Context, tries int) error {
if tries > r.MaxTries {
return errors.New("too many retries")
}
ticker := time.NewTicker(r.BackoffFor(tries))
select {
case <-ctx.Done():
ticker.Stop()
return errors.New("context has been cancelled")
case <-ticker.C:
ticker.Stop()
}
return nil
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
const (
TextPlain = "text/plain"
TextJSON = "text/json"
ApplicationJSON = "application/json"
ApplicationXML = "application/xml"
ApplicationCloudEventsJSON = "application/cloudevents+json"
ApplicationCloudEventsBatchJSON = "application/cloudevents-batch+json"
)
// StringOfApplicationJSON returns a string pointer to "application/json"
func StringOfApplicationJSON() *string {
a := ApplicationJSON
return &a
}
// StringOfApplicationXML returns a string pointer to "application/xml"
func StringOfApplicationXML() *string {
a := ApplicationXML
return &a
}
// StringOfTextPlain returns a string pointer to "text/plain"
func StringOfTextPlain() *string {
a := TextPlain
return &a
}
// StringOfApplicationCloudEventsJSON returns a string pointer to
// "application/cloudevents+json"
func StringOfApplicationCloudEventsJSON() *string {
a := ApplicationCloudEventsJSON
return &a
}
// StringOfApplicationCloudEventsBatchJSON returns a string pointer to
// "application/cloudevents-batch+json"
func StringOfApplicationCloudEventsBatchJSON() *string {
a := ApplicationCloudEventsBatchJSON
return &a
}

View File

@ -0,0 +1,16 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
const (
Base64 = "base64"
)
// StringOfBase64 returns a string pointer to "Base64"
func StringOfBase64() *string {
a := Base64
return &a
}

View File

@ -0,0 +1,78 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package datacodec
import (
"context"
"fmt"
"github.com/cloudevents/sdk-go/v2/event/datacodec/json"
"github.com/cloudevents/sdk-go/v2/event/datacodec/text"
"github.com/cloudevents/sdk-go/v2/event/datacodec/xml"
)
// Decoder is the expected function signature for decoding `in` to `out`.
// If Event sent the payload as base64, Decoder assumes that `in` is the
// decoded base64 byte array.
type Decoder func(ctx context.Context, in []byte, out interface{}) error
// Encoder is the expected function signature for encoding `in` to bytes.
// Returns an error if the encoder has an issue encoding `in`.
type Encoder func(ctx context.Context, in interface{}) ([]byte, error)
var decoder map[string]Decoder
var encoder map[string]Encoder
func init() {
decoder = make(map[string]Decoder, 10)
encoder = make(map[string]Encoder, 10)
AddDecoder("", json.Decode)
AddDecoder("application/json", json.Decode)
AddDecoder("text/json", json.Decode)
AddDecoder("application/xml", xml.Decode)
AddDecoder("text/xml", xml.Decode)
AddDecoder("text/plain", text.Decode)
AddEncoder("", json.Encode)
AddEncoder("application/json", json.Encode)
AddEncoder("text/json", json.Encode)
AddEncoder("application/xml", xml.Encode)
AddEncoder("text/xml", xml.Encode)
AddEncoder("text/plain", text.Encode)
}
// AddDecoder registers a decoder for a given content type. The codecs will use
// these to decode the data payload from a cloudevent.Event object.
func AddDecoder(contentType string, fn Decoder) {
decoder[contentType] = fn
}
// AddEncoder registers an encoder for a given content type. The codecs will
// use these to encode the data payload for a cloudevent.Event object.
func AddEncoder(contentType string, fn Encoder) {
encoder[contentType] = fn
}
// Decode looks up and invokes the decoder registered for the given content
// type. An error is returned if no decoder is registered for the given
// content type.
func Decode(ctx context.Context, contentType string, in []byte, out interface{}) error {
if fn, ok := decoder[contentType]; ok {
return fn(ctx, in, out)
}
return fmt.Errorf("[decode] unsupported content type: %q", contentType)
}
// Encode looks up and invokes the encoder registered for the given content
// type. An error is returned if no encoder is registered for the given
// content type.
func Encode(ctx context.Context, contentType string, in interface{}) ([]byte, error) {
if fn, ok := encoder[contentType]; ok {
return fn(ctx, in)
}
return nil, fmt.Errorf("[encode] unsupported content type: %q", contentType)
}

View File

@ -0,0 +1,10 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package datacodec holds the data codec registry and adds known encoders and decoders supporting media types such as
`application/json` and `application/xml`.
*/
package datacodec

View File

@ -0,0 +1,56 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package json
import (
"context"
"encoding/json"
"fmt"
"reflect"
)
// Decode takes `in` as []byte.
// If Event sent the payload as base64, Decoder assumes that `in` is the
// decoded base64 byte array.
func Decode(ctx context.Context, in []byte, out interface{}) error {
if in == nil {
return nil
}
if out == nil {
return fmt.Errorf("out is nil")
}
if err := json.Unmarshal(in, out); err != nil {
return fmt.Errorf("[json] found bytes \"%s\", but failed to unmarshal: %s", string(in), err.Error())
}
return nil
}
// Encode attempts to json.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or json.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
if in == nil {
return nil, nil
}
it := reflect.TypeOf(in)
switch it.Kind() {
case reflect.Slice:
if it.Elem().Kind() == reflect.Uint8 {
if b, ok := in.([]byte); ok && len(b) > 0 {
// check to see if it is a pre-encoded byte string.
if b[0] == byte('"') || b[0] == byte('{') || b[0] == byte('[') {
return b, nil
}
}
}
}
return json.Marshal(in)
}

View File

@ -0,0 +1,9 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package json holds the encoder/decoder implementation for `application/json`.
*/
package json

View File

@ -0,0 +1,30 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package text
import (
"context"
"fmt"
)
// Text codec converts []byte or string to string and vice-versa.
func Decode(_ context.Context, in []byte, out interface{}) error {
p, _ := out.(*string)
if p == nil {
return fmt.Errorf("text.Decode out: want *string, got %T", out)
}
*p = string(in)
return nil
}
func Encode(_ context.Context, in interface{}) ([]byte, error) {
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("text.Encode in: want string, got %T", in)
}
return []byte(s), nil
}

View File

@ -0,0 +1,9 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package text holds the encoder/decoder implementation for `text/plain`.
*/
package text

View File

@ -0,0 +1,40 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package xml
import (
"context"
"encoding/xml"
"fmt"
)
// Decode takes `in` as []byte.
// If Event sent the payload as base64, Decoder assumes that `in` is the
// decoded base64 byte array.
func Decode(ctx context.Context, in []byte, out interface{}) error {
if in == nil {
return nil
}
if err := xml.Unmarshal(in, out); err != nil {
return fmt.Errorf("[xml] found bytes, but failed to unmarshal: %s %s", err.Error(), string(in))
}
return nil
}
// Encode attempts to xml.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or xml.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
if b, ok := in.([]byte); ok {
// check to see if it is a pre-encoded byte string.
if len(b) > 0 && b[0] == byte('"') {
return b, nil
}
}
return xml.Marshal(in)
}

View File

@ -0,0 +1,9 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package xml holds the encoder/decoder implementation for `application/xml`.
*/
package xml

View File

@ -0,0 +1,9 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/*
Package event provides primitives to work with CloudEvents specification: https://github.com/cloudevents/spec.
*/
package event

View File

@ -0,0 +1,126 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"bytes"
"encoding/json"
"strings"
)
// Event represents the canonical representation of a CloudEvent.
type Event struct {
Context EventContext
DataEncoded []byte
// DataBase64 indicates if the event, when serialized, represents
// the data field using the base64 encoding.
// In v0.3, this field is superseded by DataContentEncoding
DataBase64 bool
FieldErrors map[string]error
}
const (
defaultEventVersion = CloudEventsVersionV1
)
func (e *Event) fieldError(field string, err error) {
if e.FieldErrors == nil {
e.FieldErrors = make(map[string]error)
}
e.FieldErrors[field] = err
}
func (e *Event) fieldOK(field string) {
if e.FieldErrors != nil {
delete(e.FieldErrors, field)
}
}
// New returns a new Event, an optional version can be passed to change the
// default spec version from 1.0 to the provided version.
func New(version ...string) Event {
specVersion := defaultEventVersion
if len(version) >= 1 {
specVersion = version[0]
}
e := &Event{}
e.SetSpecVersion(specVersion)
return *e
}
// ExtensionAs is deprecated: access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
func (e Event) ExtensionAs(name string, obj interface{}) error {
return e.Context.ExtensionAs(name, obj)
}
// String returns a pretty-printed representation of the Event.
func (e Event) String() string {
b := strings.Builder{}
b.WriteString(e.Context.String())
if e.DataEncoded != nil {
if e.DataBase64 {
b.WriteString("Data (binary),\n ")
} else {
b.WriteString("Data,\n ")
}
switch e.DataMediaType() {
case ApplicationJSON:
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, e.DataEncoded, " ", " ")
if err != nil {
b.Write(e.DataEncoded)
} else {
b.Write(prettyJSON.Bytes())
}
default:
b.Write(e.DataEncoded)
}
b.WriteString("\n")
}
return b.String()
}
func (e Event) Clone() Event {
out := Event{}
out.Context = e.Context.Clone()
out.DataEncoded = cloneBytes(e.DataEncoded)
out.DataBase64 = e.DataBase64
out.FieldErrors = e.cloneFieldErrors()
return out
}
func cloneBytes(in []byte) []byte {
if in == nil {
return nil
}
out := make([]byte, len(in))
copy(out, in)
return out
}
func (e Event) cloneFieldErrors() map[string]error {
if e.FieldErrors == nil {
return nil
}
newFE := make(map[string]error, len(e.FieldErrors))
for k, v := range e.FieldErrors {
newFE[k] = v
}
return newFE
}

View File

@ -0,0 +1,118 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"github.com/cloudevents/sdk-go/v2/event/datacodec"
)
// SetData encodes the given payload with the given content type.
// If the provided payload is a byte array, when marshalled to json it will be encoded as base64.
// If the provided payload is different from byte array, datacodec.Encode is invoked to attempt a
// marshalling to byte array.
func (e *Event) SetData(contentType string, obj interface{}) error {
e.SetDataContentType(contentType)
if e.SpecVersion() != CloudEventsVersionV1 {
return e.legacySetData(obj)
}
// Version 1.0 and above.
switch obj := obj.(type) {
case []byte:
e.DataEncoded = obj
e.DataBase64 = true
default:
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.DataEncoded = data
e.DataBase64 = false
}
return nil
}
// Deprecated: Delete when we do not have to support Spec v0.3.
func (e *Event) legacySetData(obj interface{}) error {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
if e.DeprecatedDataContentEncoding() == Base64 {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
e.DataEncoded = buf
e.DataBase64 = false
} else {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.DataEncoded = data
e.DataBase64 = false
}
return nil
}
const (
quotes = `"'`
)
func (e Event) Data() []byte {
return e.DataEncoded
}
// DataAs attempts to populate the provided data object with the event payload.
// obj should be a pointer type.
func (e Event) DataAs(obj interface{}) error {
data := e.Data()
if len(data) == 0 {
// No data.
return nil
}
if e.SpecVersion() != CloudEventsVersionV1 {
var err error
if data, err = e.legacyConvertData(data); err != nil {
return err
}
}
return datacodec.Decode(context.Background(), e.DataMediaType(), data, obj)
}
func (e Event) legacyConvertData(data []byte) ([]byte, error) {
if e.Context.DeprecatedGetDataContentEncoding() == Base64 {
var bs []byte
// test to see if we need to unquote the data.
if data[0] == quotes[0] || data[0] == quotes[1] {
str, err := strconv.Unquote(string(data))
if err != nil {
return nil, err
}
bs = []byte(str)
} else {
bs = data
}
buf := make([]byte, base64.StdEncoding.DecodedLen(len(bs)))
n, err := base64.StdEncoding.Decode(buf, bs)
if err != nil {
return nil, fmt.Errorf("failed to decode data from base64: %s", err.Error())
}
data = buf[:n]
}
return data, nil
}

View File

@ -0,0 +1,102 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"time"
)
// EventReader is the interface for reading through an event from attributes.
type EventReader interface {
// SpecVersion returns event.Context.GetSpecVersion().
SpecVersion() string
// Type returns event.Context.GetType().
Type() string
// Source returns event.Context.GetSource().
Source() string
// Subject returns event.Context.GetSubject().
Subject() string
// ID returns event.Context.GetID().
ID() string
// Time returns event.Context.GetTime().
Time() time.Time
// DataSchema returns event.Context.GetDataSchema().
DataSchema() string
// DataContentType returns event.Context.GetDataContentType().
DataContentType() string
// DataMediaType returns event.Context.GetDataMediaType().
DataMediaType() string
// DeprecatedDataContentEncoding returns event.Context.DeprecatedGetDataContentEncoding().
DeprecatedDataContentEncoding() string
// Extension Attributes
// Extensions returns the event.Context.GetExtensions().
// Extensions use the CloudEvents type system, details in package cloudevents/types.
Extensions() map[string]interface{}
// ExtensionAs returns event.Context.ExtensionAs(name, obj).
//
// DEPRECATED: Access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
ExtensionAs(string, interface{}) error
// Data Attribute
// Data returns the raw data buffer
// If the event was encoded with base64 encoding, Data returns the already decoded
// byte array
Data() []byte
// DataAs attempts to populate the provided data object with the event payload.
DataAs(interface{}) error
}
// EventWriter is the interface for writing through an event onto attributes.
// If an error is thrown by a sub-component, EventWriter caches the error
// internally and exposes errors with a call to event.Validate().
type EventWriter interface {
// Context Attributes
// SetSpecVersion performs event.Context.SetSpecVersion.
SetSpecVersion(string)
// SetType performs event.Context.SetType.
SetType(string)
// SetSource performs event.Context.SetSource.
SetSource(string)
// SetSubject( performs event.Context.SetSubject.
SetSubject(string)
// SetID performs event.Context.SetID.
SetID(string)
// SetTime performs event.Context.SetTime.
SetTime(time.Time)
// SetDataSchema performs event.Context.SetDataSchema.
SetDataSchema(string)
// SetDataContentType performs event.Context.SetDataContentType.
SetDataContentType(string)
// DeprecatedSetDataContentEncoding performs event.Context.DeprecatedSetDataContentEncoding.
SetDataContentEncoding(string)
// Extension Attributes
// SetExtension performs event.Context.SetExtension.
SetExtension(string, interface{})
// SetData encodes the given payload with the given content type.
// If the provided payload is a byte array, when marshalled to json it will be encoded as base64.
// If the provided payload is different from byte array, datacodec.Encode is invoked to attempt a
// marshalling to byte array.
SetData(string, interface{}) error
}

View File

@ -0,0 +1,203 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"strings"
jsoniter "github.com/json-iterator/go"
)
// WriteJson writes the in event in the provided writer.
// Note: this function assumes the input event is valid.
func WriteJson(in *Event, writer io.Writer) error {
stream := jsoniter.ConfigFastest.BorrowStream(writer)
defer jsoniter.ConfigFastest.ReturnStream(stream)
stream.WriteObjectStart()
var ext map[string]interface{}
var dct *string
var isBase64 bool
// Write the context (without the extensions)
switch eventContext := in.Context.(type) {
case *EventContextV03:
// Set a bunch of variables we need later
ext = eventContext.Extensions
dct = eventContext.DataContentType
stream.WriteObjectField("specversion")
stream.WriteString(CloudEventsVersionV03)
stream.WriteMore()
stream.WriteObjectField("id")
stream.WriteString(eventContext.ID)
stream.WriteMore()
stream.WriteObjectField("source")
stream.WriteString(eventContext.Source.String())
stream.WriteMore()
stream.WriteObjectField("type")
stream.WriteString(eventContext.Type)
if eventContext.Subject != nil {
stream.WriteMore()
stream.WriteObjectField("subject")
stream.WriteString(*eventContext.Subject)
}
if eventContext.DataContentEncoding != nil {
isBase64 = true
stream.WriteMore()
stream.WriteObjectField("datacontentencoding")
stream.WriteString(*eventContext.DataContentEncoding)
}
if eventContext.DataContentType != nil {
stream.WriteMore()
stream.WriteObjectField("datacontenttype")
stream.WriteString(*eventContext.DataContentType)
}
if eventContext.SchemaURL != nil {
stream.WriteMore()
stream.WriteObjectField("schemaurl")
stream.WriteString(eventContext.SchemaURL.String())
}
if eventContext.Time != nil {
stream.WriteMore()
stream.WriteObjectField("time")
stream.WriteString(eventContext.Time.String())
}
case *EventContextV1:
// Set a bunch of variables we need later
ext = eventContext.Extensions
dct = eventContext.DataContentType
isBase64 = in.DataBase64
stream.WriteObjectField("specversion")
stream.WriteString(CloudEventsVersionV1)
stream.WriteMore()
stream.WriteObjectField("id")
stream.WriteString(eventContext.ID)
stream.WriteMore()
stream.WriteObjectField("source")
stream.WriteString(eventContext.Source.String())
stream.WriteMore()
stream.WriteObjectField("type")
stream.WriteString(eventContext.Type)
if eventContext.Subject != nil {
stream.WriteMore()
stream.WriteObjectField("subject")
stream.WriteString(*eventContext.Subject)
}
if eventContext.DataContentType != nil {
stream.WriteMore()
stream.WriteObjectField("datacontenttype")
stream.WriteString(*eventContext.DataContentType)
}
if eventContext.DataSchema != nil {
stream.WriteMore()
stream.WriteObjectField("dataschema")
stream.WriteString(eventContext.DataSchema.String())
}
if eventContext.Time != nil {
stream.WriteMore()
stream.WriteObjectField("time")
stream.WriteString(eventContext.Time.String())
}
default:
return fmt.Errorf("missing event context")
}
// Let's do a check on the error
if stream.Error != nil {
return fmt.Errorf("error while writing the event attributes: %w", stream.Error)
}
// Let's write the body
if in.DataEncoded != nil {
stream.WriteMore()
// We need to figure out the media type first
var mediaType string
if dct == nil {
mediaType = ApplicationJSON
} else {
// This code is required to extract the media type from the full content type string (which might contain encoding and stuff)
contentType := *dct
i := strings.IndexRune(contentType, ';')
if i == -1 {
i = len(contentType)
}
mediaType = strings.TrimSpace(strings.ToLower(contentType[0:i]))
}
isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON
// If isJson and no encoding to base64, we don't need to perform additional steps
if isJson && !isBase64 {
stream.WriteObjectField("data")
_, err := stream.Write(in.DataEncoded)
if err != nil {
return fmt.Errorf("error while writing data: %w", err)
}
} else {
if in.Context.GetSpecVersion() == CloudEventsVersionV1 && isBase64 {
stream.WriteObjectField("data_base64")
} else {
stream.WriteObjectField("data")
}
// At this point of we need to write to base 64 string, or we just need to write the plain string
if isBase64 {
stream.WriteString(base64.StdEncoding.EncodeToString(in.DataEncoded))
} else {
stream.WriteString(string(in.DataEncoded))
}
}
}
// Let's do a check on the error
if stream.Error != nil {
return fmt.Errorf("error while writing the event data: %w", stream.Error)
}
for k, v := range ext {
stream.WriteMore()
stream.WriteObjectField(k)
stream.WriteVal(v)
}
stream.WriteObjectEnd()
// Let's do a check on the error
if stream.Error != nil {
return fmt.Errorf("error while writing the event extensions: %w", stream.Error)
}
return stream.Flush()
}
// MarshalJSON implements a custom json marshal method used when this type is
// marshaled using json.Marshal.
func (e Event) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
err := WriteJson(&e, &buf)
return buf.Bytes(), err
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"time"
)
var _ EventReader = (*Event)(nil)
// SpecVersion implements EventReader.SpecVersion
func (e Event) SpecVersion() string {
if e.Context != nil {
return e.Context.GetSpecVersion()
}
return ""
}
// Type implements EventReader.Type
func (e Event) Type() string {
if e.Context != nil {
return e.Context.GetType()
}
return ""
}
// Source implements EventReader.Source
func (e Event) Source() string {
if e.Context != nil {
return e.Context.GetSource()
}
return ""
}
// Subject implements EventReader.Subject
func (e Event) Subject() string {
if e.Context != nil {
return e.Context.GetSubject()
}
return ""
}
// ID implements EventReader.ID
func (e Event) ID() string {
if e.Context != nil {
return e.Context.GetID()
}
return ""
}
// Time implements EventReader.Time
func (e Event) Time() time.Time {
if e.Context != nil {
return e.Context.GetTime()
}
return time.Time{}
}
// DataSchema implements EventReader.DataSchema
func (e Event) DataSchema() string {
if e.Context != nil {
return e.Context.GetDataSchema()
}
return ""
}
// DataContentType implements EventReader.DataContentType
func (e Event) DataContentType() string {
if e.Context != nil {
return e.Context.GetDataContentType()
}
return ""
}
// DataMediaType returns the parsed DataMediaType of the event. If parsing
// fails, the empty string is returned. To retrieve the parsing error, use
// `Context.GetDataMediaType` instead.
func (e Event) DataMediaType() string {
if e.Context != nil {
mediaType, _ := e.Context.GetDataMediaType()
return mediaType
}
return ""
}
// DeprecatedDataContentEncoding implements EventReader.DeprecatedDataContentEncoding
func (e Event) DeprecatedDataContentEncoding() string {
if e.Context != nil {
return e.Context.DeprecatedGetDataContentEncoding()
}
return ""
}
// Extensions implements EventReader.Extensions
func (e Event) Extensions() map[string]interface{} {
if e.Context != nil {
return e.Context.GetExtensions()
}
return map[string]interface{}(nil)
}

View File

@ -0,0 +1,480 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"encoding/base64"
"errors"
"fmt"
"io"
"sync"
jsoniter "github.com/json-iterator/go"
"github.com/cloudevents/sdk-go/v2/types"
)
const specVersionV03Flag uint8 = 1 << 4
const specVersionV1Flag uint8 = 1 << 5
const dataBase64Flag uint8 = 1 << 6
const dataContentTypeFlag uint8 = 1 << 7
func checkFlag(state uint8, flag uint8) bool {
return state&flag != 0
}
func appendFlag(state *uint8, flag uint8) {
*state = (*state) | flag
}
var iterPool = sync.Pool{
New: func() interface{} {
return jsoniter.Parse(jsoniter.ConfigFastest, nil, 1024)
},
}
func borrowIterator(reader io.Reader) *jsoniter.Iterator {
iter := iterPool.Get().(*jsoniter.Iterator)
iter.Reset(reader)
return iter
}
func returnIterator(iter *jsoniter.Iterator) {
iter.Error = nil
iter.Attachment = nil
iterPool.Put(iter)
}
func ReadJson(out *Event, reader io.Reader) error {
iterator := borrowIterator(reader)
defer returnIterator(iterator)
return readJsonFromIterator(out, iterator)
}
// ReadJson allows you to read the bytes reader as an event
func readJsonFromIterator(out *Event, iterator *jsoniter.Iterator) error {
// Parsing dependency graph:
// SpecVersion
// ^ ^
// | +--------------+
// + +
// All Attributes datacontenttype (and datacontentencoding for v0.3)
// (except datacontenttype) ^
// |
// |
// +
// Data
var state uint8 = 0
var cachedData []byte
var (
// Universally parseable fields.
id string
typ string
source types.URIRef
subject *string
time *types.Timestamp
datacontenttype *string
extensions = make(map[string]interface{})
// These fields require knowledge about the specversion to be parsed.
schemaurl jsoniter.Any
datacontentencoding jsoniter.Any
dataschema jsoniter.Any
dataBase64 jsoniter.Any
)
for key := iterator.ReadObject(); key != ""; key = iterator.ReadObject() {
// Check if we have some error in our error cache
if iterator.Error != nil {
return iterator.Error
}
// We have a key, now we need to figure out what to do
// depending on the parsing state
// If it's a specversion, trigger state change
if key == "specversion" {
if checkFlag(state, specVersionV1Flag|specVersionV03Flag) {
return fmt.Errorf("specversion was already provided")
}
sv := iterator.ReadString()
// Check proper specversion
switch sv {
case CloudEventsVersionV1:
con := &EventContextV1{
ID: id,
Type: typ,
Source: source,
Subject: subject,
Time: time,
DataContentType: datacontenttype,
}
// Add the fields relevant for the version ...
if dataschema != nil {
var err error
con.DataSchema, err = toUriPtr(dataschema)
if err != nil {
return err
}
}
if dataBase64 != nil {
stream := jsoniter.ConfigFastest.BorrowStream(nil)
defer jsoniter.ConfigFastest.ReturnStream(stream)
dataBase64.WriteTo(stream)
cachedData = stream.Buffer()
if stream.Error != nil {
return stream.Error
}
appendFlag(&state, dataBase64Flag)
}
// ... add all remaining fields as extensions.
if schemaurl != nil {
extensions["schemaurl"] = schemaurl.GetInterface()
}
if datacontentencoding != nil {
extensions["datacontentencoding"] = datacontentencoding.GetInterface()
}
out.Context = con
appendFlag(&state, specVersionV1Flag)
case CloudEventsVersionV03:
con := &EventContextV03{
ID: id,
Type: typ,
Source: source,
Subject: subject,
Time: time,
DataContentType: datacontenttype,
}
var err error
// Add the fields relevant for the version ...
if schemaurl != nil {
con.SchemaURL, err = toUriRefPtr(schemaurl)
if err != nil {
return err
}
}
if datacontentencoding != nil {
con.DataContentEncoding, err = toStrPtr(datacontentencoding)
if *con.DataContentEncoding != Base64 {
err = ValidationError{"datacontentencoding": errors.New("invalid datacontentencoding value, the only allowed value is 'base64'")}
}
if err != nil {
return err
}
appendFlag(&state, dataBase64Flag)
}
// ... add all remaining fields as extensions.
if dataschema != nil {
extensions["dataschema"] = dataschema.GetInterface()
}
if dataBase64 != nil {
extensions["data_base64"] = dataBase64.GetInterface()
}
out.Context = con
appendFlag(&state, specVersionV03Flag)
default:
return ValidationError{"specversion": errors.New("unknown value: " + sv)}
}
// Apply all extensions to the context object.
for key, val := range extensions {
if err := out.Context.SetExtension(key, val); err != nil {
return err
}
}
continue
}
// If no specversion ...
if !checkFlag(state, specVersionV03Flag|specVersionV1Flag) {
switch key {
case "id":
id = iterator.ReadString()
case "type":
typ = iterator.ReadString()
case "source":
source = readUriRef(iterator)
case "subject":
subject = readStrPtr(iterator)
case "time":
time = readTimestamp(iterator)
case "datacontenttype":
datacontenttype = readStrPtr(iterator)
appendFlag(&state, dataContentTypeFlag)
case "data":
cachedData = iterator.SkipAndReturnBytes()
case "data_base64":
dataBase64 = iterator.ReadAny()
case "dataschema":
dataschema = iterator.ReadAny()
case "schemaurl":
schemaurl = iterator.ReadAny()
case "datacontentencoding":
datacontentencoding = iterator.ReadAny()
default:
extensions[key] = iterator.Read()
}
continue
}
// From this point downward -> we can assume the event has a context pointer non nil
// If it's a datacontenttype, trigger state change
if key == "datacontenttype" {
if checkFlag(state, dataContentTypeFlag) {
return fmt.Errorf("datacontenttype was already provided")
}
dct := iterator.ReadString()
switch ctx := out.Context.(type) {
case *EventContextV03:
ctx.DataContentType = &dct
case *EventContextV1:
ctx.DataContentType = &dct
}
appendFlag(&state, dataContentTypeFlag)
continue
}
// If it's a datacontentencoding and it's v0.3, trigger state change
if checkFlag(state, specVersionV03Flag) && key == "datacontentencoding" {
if checkFlag(state, dataBase64Flag) {
return ValidationError{"datacontentencoding": errors.New("datacontentencoding was specified twice")}
}
dce := iterator.ReadString()
if dce != Base64 {
return ValidationError{"datacontentencoding": errors.New("invalid datacontentencoding value, the only allowed value is 'base64'")}
}
out.Context.(*EventContextV03).DataContentEncoding = &dce
appendFlag(&state, dataBase64Flag)
continue
}
// We can parse all attributes, except data.
// If it's data or data_base64 and we don't have the attributes to process it, then we cache it
// The expanded form of this condition is:
// (checkFlag(state, specVersionV1Flag) && !checkFlag(state, dataContentTypeFlag) && (key == "data" || key == "data_base64")) ||
// (checkFlag(state, specVersionV03Flag) && !(checkFlag(state, dataContentTypeFlag) && checkFlag(state, dataBase64Flag)) && key == "data")
if (state&(specVersionV1Flag|dataContentTypeFlag) == specVersionV1Flag && (key == "data" || key == "data_base64")) ||
((state&specVersionV03Flag == specVersionV03Flag) && (state&(dataContentTypeFlag|dataBase64Flag) != (dataContentTypeFlag | dataBase64Flag)) && key == "data") {
if key == "data_base64" {
appendFlag(&state, dataBase64Flag)
}
cachedData = iterator.SkipAndReturnBytes()
continue
}
// At this point or this value is an attribute (excluding datacontenttype and datacontentencoding), or this value is data and this condition is valid:
// (specVersionV1Flag & dataContentTypeFlag) || (specVersionV03Flag & dataContentTypeFlag & dataBase64Flag)
switch eventContext := out.Context.(type) {
case *EventContextV03:
switch key {
case "id":
eventContext.ID = iterator.ReadString()
case "type":
eventContext.Type = iterator.ReadString()
case "source":
eventContext.Source = readUriRef(iterator)
case "subject":
eventContext.Subject = readStrPtr(iterator)
case "time":
eventContext.Time = readTimestamp(iterator)
case "schemaurl":
eventContext.SchemaURL = readUriRefPtr(iterator)
case "data":
iterator.Error = consumeData(out, checkFlag(state, dataBase64Flag), iterator)
default:
if eventContext.Extensions == nil {
eventContext.Extensions = make(map[string]interface{}, 1)
}
iterator.Error = eventContext.SetExtension(key, iterator.Read())
}
case *EventContextV1:
switch key {
case "id":
eventContext.ID = iterator.ReadString()
case "type":
eventContext.Type = iterator.ReadString()
case "source":
eventContext.Source = readUriRef(iterator)
case "subject":
eventContext.Subject = readStrPtr(iterator)
case "time":
eventContext.Time = readTimestamp(iterator)
case "dataschema":
eventContext.DataSchema = readUriPtr(iterator)
case "data":
iterator.Error = consumeData(out, false, iterator)
case "data_base64":
iterator.Error = consumeData(out, true, iterator)
default:
if eventContext.Extensions == nil {
eventContext.Extensions = make(map[string]interface{}, 1)
}
iterator.Error = eventContext.SetExtension(key, iterator.Read())
}
}
}
if state&(specVersionV03Flag|specVersionV1Flag) == 0 {
return ValidationError{"specversion": errors.New("no specversion")}
}
if iterator.Error != nil {
return iterator.Error
}
// If there is a dataToken cached, we always defer at the end the processing
// because nor datacontenttype or datacontentencoding are mandatory.
if cachedData != nil {
return consumeDataAsBytes(out, checkFlag(state, dataBase64Flag), cachedData)
}
return nil
}
func consumeDataAsBytes(e *Event, isBase64 bool, b []byte) error {
if isBase64 {
e.DataBase64 = true
// Allocate payload byte buffer
base64Encoded := b[1 : len(b)-1] // remove quotes
e.DataEncoded = make([]byte, base64.StdEncoding.DecodedLen(len(base64Encoded)))
length, err := base64.StdEncoding.Decode(e.DataEncoded, base64Encoded)
if err != nil {
return err
}
e.DataEncoded = e.DataEncoded[0:length]
return nil
}
mt, _ := e.Context.GetDataMediaType()
// Empty content type assumes json
if mt != "" && mt != ApplicationJSON && mt != TextJSON {
// If not json, then data is encoded as string
iter := jsoniter.ParseBytes(jsoniter.ConfigFastest, b)
src := iter.ReadString() // handles escaping
e.DataEncoded = []byte(src)
if iter.Error != nil {
return fmt.Errorf("unexpected data payload for media type %q, expected a string: %w", mt, iter.Error)
}
return nil
}
e.DataEncoded = b
return nil
}
func consumeData(e *Event, isBase64 bool, iter *jsoniter.Iterator) error {
if isBase64 {
e.DataBase64 = true
// Allocate payload byte buffer
base64Encoded := iter.ReadStringAsSlice()
e.DataEncoded = make([]byte, base64.StdEncoding.DecodedLen(len(base64Encoded)))
length, err := base64.StdEncoding.Decode(e.DataEncoded, base64Encoded)
if err != nil {
return err
}
e.DataEncoded = e.DataEncoded[0:length]
return nil
}
mt, _ := e.Context.GetDataMediaType()
if mt != ApplicationJSON && mt != TextJSON {
// If not json, then data is encoded as string
src := iter.ReadString() // handles escaping
e.DataEncoded = []byte(src)
if iter.Error != nil {
return fmt.Errorf("unexpected data payload for media type %q, expected a string: %w", mt, iter.Error)
}
return nil
}
e.DataEncoded = iter.SkipAndReturnBytes()
return nil
}
func readUriRef(iter *jsoniter.Iterator) types.URIRef {
str := iter.ReadString()
uriRef := types.ParseURIRef(str)
if uriRef == nil {
iter.Error = fmt.Errorf("cannot parse uri ref: %v", str)
return types.URIRef{}
}
return *uriRef
}
func readStrPtr(iter *jsoniter.Iterator) *string {
str := iter.ReadString()
if str == "" {
return nil
}
return &str
}
func readUriRefPtr(iter *jsoniter.Iterator) *types.URIRef {
return types.ParseURIRef(iter.ReadString())
}
func readUriPtr(iter *jsoniter.Iterator) *types.URI {
return types.ParseURI(iter.ReadString())
}
func readTimestamp(iter *jsoniter.Iterator) *types.Timestamp {
t, err := types.ParseTimestamp(iter.ReadString())
if err != nil {
iter.Error = err
}
return t
}
func toStrPtr(val jsoniter.Any) (*string, error) {
str := val.ToString()
if val.LastError() != nil {
return nil, val.LastError()
}
if str == "" {
return nil, nil
}
return &str, nil
}
func toUriRefPtr(val jsoniter.Any) (*types.URIRef, error) {
str := val.ToString()
if val.LastError() != nil {
return nil, val.LastError()
}
return types.ParseURIRef(str), nil
}
func toUriPtr(val jsoniter.Any) (*types.URI, error) {
str := val.ToString()
if val.LastError() != nil {
return nil, val.LastError()
}
return types.ParseURI(str), nil
}
// UnmarshalJSON implements the json unmarshal method used when this type is
// unmarshaled using json.Unmarshal.
func (e *Event) UnmarshalJSON(b []byte) error {
iterator := jsoniter.ConfigFastest.BorrowIterator(b)
defer jsoniter.ConfigFastest.ReturnIterator(iterator)
return readJsonFromIterator(e, iterator)
}

View File

@ -0,0 +1,50 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"fmt"
"strings"
)
type ValidationError map[string]error
func (e ValidationError) Error() string {
b := strings.Builder{}
for k, v := range e {
b.WriteString(k)
b.WriteString(": ")
b.WriteString(v.Error())
b.WriteRune('\n')
}
return b.String()
}
// Validate performs a spec based validation on this event.
// Validation is dependent on the spec version specified in the event context.
func (e Event) Validate() error {
if e.Context == nil {
return ValidationError{"specversion": fmt.Errorf("missing Event.Context")}
}
errs := map[string]error{}
if e.FieldErrors != nil {
for k, v := range e.FieldErrors {
errs[k] = v
}
}
if fieldErrors := e.Context.Validate(); fieldErrors != nil {
for k, v := range fieldErrors {
errs[k] = v
}
}
if len(errs) > 0 {
return ValidationError(errs)
}
return nil
}

View File

@ -0,0 +1,117 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"fmt"
"time"
)
var _ EventWriter = (*Event)(nil)
// SetSpecVersion implements EventWriter.SetSpecVersion
func (e *Event) SetSpecVersion(v string) {
switch v {
case CloudEventsVersionV03:
if e.Context == nil {
e.Context = &EventContextV03{}
} else {
e.Context = e.Context.AsV03()
}
case CloudEventsVersionV1:
if e.Context == nil {
e.Context = &EventContextV1{}
} else {
e.Context = e.Context.AsV1()
}
default:
e.fieldError("specversion", fmt.Errorf("a valid spec version is required: [%s, %s]",
CloudEventsVersionV03, CloudEventsVersionV1))
return
}
e.fieldOK("specversion")
}
// SetType implements EventWriter.SetType
func (e *Event) SetType(t string) {
if err := e.Context.SetType(t); err != nil {
e.fieldError("type", err)
} else {
e.fieldOK("type")
}
}
// SetSource implements EventWriter.SetSource
func (e *Event) SetSource(s string) {
if err := e.Context.SetSource(s); err != nil {
e.fieldError("source", err)
} else {
e.fieldOK("source")
}
}
// SetSubject implements EventWriter.SetSubject
func (e *Event) SetSubject(s string) {
if err := e.Context.SetSubject(s); err != nil {
e.fieldError("subject", err)
} else {
e.fieldOK("subject")
}
}
// SetID implements EventWriter.SetID
func (e *Event) SetID(id string) {
if err := e.Context.SetID(id); err != nil {
e.fieldError("id", err)
} else {
e.fieldOK("id")
}
}
// SetTime implements EventWriter.SetTime
func (e *Event) SetTime(t time.Time) {
if err := e.Context.SetTime(t); err != nil {
e.fieldError("time", err)
} else {
e.fieldOK("time")
}
}
// SetDataSchema implements EventWriter.SetDataSchema
func (e *Event) SetDataSchema(s string) {
if err := e.Context.SetDataSchema(s); err != nil {
e.fieldError("dataschema", err)
} else {
e.fieldOK("dataschema")
}
}
// SetDataContentType implements EventWriter.SetDataContentType
func (e *Event) SetDataContentType(ct string) {
if err := e.Context.SetDataContentType(ct); err != nil {
e.fieldError("datacontenttype", err)
} else {
e.fieldOK("datacontenttype")
}
}
// SetDataContentEncoding is deprecated. Implements EventWriter.SetDataContentEncoding.
func (e *Event) SetDataContentEncoding(enc string) {
if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil {
e.fieldError("datacontentencoding", err)
} else {
e.fieldOK("datacontentencoding")
}
}
// SetExtension implements EventWriter.SetExtension
func (e *Event) SetExtension(name string, obj interface{}) {
if err := e.Context.SetExtension(name, obj); err != nil {
e.fieldError("extension:"+name, err)
} else {
e.fieldOK("extension:" + name)
}
}

View File

@ -0,0 +1,125 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import "time"
// EventContextReader are the methods required to be a reader of context
// attributes.
type EventContextReader interface {
// GetSpecVersion returns the native CloudEvents Spec version of the event
// context.
GetSpecVersion() string
// GetType returns the CloudEvents type from the context.
GetType() string
// GetSource returns the CloudEvents source from the context.
GetSource() string
// GetSubject returns the CloudEvents subject from the context.
GetSubject() string
// GetID returns the CloudEvents ID from the context.
GetID() string
// GetTime returns the CloudEvents creation time from the context.
GetTime() time.Time
// GetDataSchema returns the CloudEvents schema URL (if any) from the
// context.
GetDataSchema() string
// GetDataContentType returns content type on the context.
GetDataContentType() string
// DeprecatedGetDataContentEncoding returns content encoding on the context.
DeprecatedGetDataContentEncoding() string
// GetDataMediaType returns the MIME media type for encoded data, which is
// needed by both encoding and decoding. This is a processed form of
// GetDataContentType and it may return an error.
GetDataMediaType() (string, error)
// DEPRECATED: Access extensions directly via the GetExtensions()
// For example replace this:
//
// var i int
// err := ec.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(ec.GetExtensions["foo"])
//
ExtensionAs(string, interface{}) error
// GetExtensions returns the full extensions map.
//
// Extensions use the CloudEvents type system, details in package cloudevents/types.
GetExtensions() map[string]interface{}
// GetExtension returns the extension associated with with the given key.
// The given key is case insensitive. If the extension can not be found,
// an error will be returned.
GetExtension(string) (interface{}, error)
}
// EventContextWriter are the methods required to be a writer of context
// attributes.
type EventContextWriter interface {
// SetType sets the type of the context.
SetType(string) error
// SetSource sets the source of the context.
SetSource(string) error
// SetSubject sets the subject of the context.
SetSubject(string) error
// SetID sets the ID of the context.
SetID(string) error
// SetTime sets the time of the context.
SetTime(time time.Time) error
// SetDataSchema sets the schema url of the context.
SetDataSchema(string) error
// SetDataContentType sets the data content type of the context.
SetDataContentType(string) error
// DeprecatedSetDataContentEncoding sets the data context encoding of the context.
DeprecatedSetDataContentEncoding(string) error
// SetExtension sets the given interface onto the extension attributes
// determined by the provided name.
//
// This function fails in V1 if the name doesn't respect the regex ^[a-zA-Z0-9]+$
//
// Package ./types documents the types that are allowed as extension values.
SetExtension(string, interface{}) error
}
// EventContextConverter are the methods that allow for event version
// conversion.
type EventContextConverter interface {
// AsV03 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v0.3 field names, moving fields to or
// from extensions as necessary.
AsV03() *EventContextV03
// AsV1 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v1.0 field names, moving fields to or
// from extensions as necessary.
AsV1() *EventContextV1
}
// EventContext is conical interface for a CloudEvents Context.
type EventContext interface {
// EventContextConverter allows for conversion between versions.
EventContextConverter
// EventContextReader adds methods for reading context.
EventContextReader
// EventContextWriter adds methods for writing to context.
EventContextWriter
// Validate the event based on the specifics of the CloudEvents spec version
// represented by this event context.
Validate() ValidationError
// Clone clones the event context.
Clone() EventContext
// String returns a pretty-printed representation of the EventContext.
String() string
}

View File

@ -0,0 +1,329 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"encoding/json"
"fmt"
"mime"
"sort"
"strings"
"github.com/cloudevents/sdk-go/v2/types"
)
const (
// CloudEventsVersionV03 represents the version 0.3 of the CloudEvents spec.
CloudEventsVersionV03 = "0.3"
)
var specV03Attributes = map[string]struct{}{
"type": {},
"source": {},
"subject": {},
"id": {},
"time": {},
"schemaurl": {},
"datacontenttype": {},
"datacontentencoding": {},
}
// EventContextV03 represents the non-data attributes of a CloudEvents v0.3
// event.
type EventContextV03 struct {
// Type - The type of the occurrence which has happened.
Type string `json:"type"`
// Source - A URI describing the event producer.
Source types.URIRef `json:"source"`
// Subject - The subject of the event in the context of the event producer
// (identified by `source`).
Subject *string `json:"subject,omitempty"`
// ID of the event; must be non-empty and unique within the scope of the producer.
ID string `json:"id"`
// Time - A Timestamp when the event happened.
Time *types.Timestamp `json:"time,omitempty"`
// DataSchema - A link to the schema that the `data` attribute adheres to.
SchemaURL *types.URIRef `json:"schemaurl,omitempty"`
// GetDataMediaType - A MIME (RFC2046) string describing the media type of `data`.
DataContentType *string `json:"datacontenttype,omitempty"`
// DeprecatedDataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`.
DataContentEncoding *string `json:"datacontentencoding,omitempty"`
// Extensions - Additional extension metadata beyond the base spec.
Extensions map[string]interface{} `json:"-"`
}
// Adhere to EventContext
var _ EventContext = (*EventContextV03)(nil)
// ExtensionAs implements EventContext.ExtensionAs
func (ec EventContextV03) ExtensionAs(name string, obj interface{}) error {
value, ok := ec.Extensions[name]
if !ok {
return fmt.Errorf("extension %q does not exist", name)
}
// Try to unmarshal extension if we find it as a RawMessage.
switch v := value.(type) {
case json.RawMessage:
if err := json.Unmarshal(v, obj); err == nil {
// if that worked, return with obj set.
return nil
}
}
// else try as a string ptr.
// Only support *string for now.
switch v := obj.(type) {
case *string:
if valueAsString, ok := value.(string); ok {
*v = valueAsString
return nil
} else {
return fmt.Errorf("invalid type for extension %q", name)
}
default:
return fmt.Errorf("unknown extension type %T", obj)
}
}
// SetExtension adds the extension 'name' with value 'value' to the CloudEvents
// context. This function fails if the name uses a reserved event context key.
func (ec *EventContextV03) SetExtension(name string, value interface{}) error {
if ec.Extensions == nil {
ec.Extensions = make(map[string]interface{})
}
if _, ok := specV03Attributes[strings.ToLower(name)]; ok {
return fmt.Errorf("bad key %q: CloudEvents spec attribute MUST NOT be overwritten by extension", name)
}
if value == nil {
delete(ec.Extensions, name)
if len(ec.Extensions) == 0 {
ec.Extensions = nil
}
return nil
} else {
v, err := types.Validate(value)
if err == nil {
ec.Extensions[name] = v
}
return err
}
}
// Clone implements EventContextConverter.Clone
func (ec EventContextV03) Clone() EventContext {
ec03 := ec.AsV03()
ec03.Source = types.Clone(ec.Source).(types.URIRef)
if ec.Time != nil {
ec03.Time = types.Clone(ec.Time).(*types.Timestamp)
}
if ec.SchemaURL != nil {
ec03.SchemaURL = types.Clone(ec.SchemaURL).(*types.URIRef)
}
ec03.Extensions = ec.cloneExtensions()
return ec03
}
func (ec *EventContextV03) cloneExtensions() map[string]interface{} {
old := ec.Extensions
if old == nil {
return nil
}
new := make(map[string]interface{}, len(ec.Extensions))
for k, v := range old {
new[k] = types.Clone(v)
}
return new
}
// AsV03 implements EventContextConverter.AsV03
func (ec EventContextV03) AsV03() *EventContextV03 {
return &ec
}
// AsV1 implements EventContextConverter.AsV1
func (ec EventContextV03) AsV1() *EventContextV1 {
ret := EventContextV1{
ID: ec.ID,
Time: ec.Time,
Type: ec.Type,
DataContentType: ec.DataContentType,
Source: types.URIRef{URL: ec.Source.URL},
Subject: ec.Subject,
Extensions: make(map[string]interface{}),
}
if ec.SchemaURL != nil {
ret.DataSchema = &types.URI{URL: ec.SchemaURL.URL}
}
// DataContentEncoding was removed in 1.0, so put it in an extension for 1.0.
if ec.DataContentEncoding != nil {
_ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding)
}
if ec.Extensions != nil {
for k, v := range ec.Extensions {
k = strings.ToLower(k)
ret.Extensions[k] = v
}
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/master/spec.md
// As of Feb 26, 2019, commit 17c32ea26baf7714ad027d9917d03d2fff79fc7e
// + https://github.com/cloudevents/spec/pull/387 -> datacontentencoding
// + https://github.com/cloudevents/spec/pull/406 -> subject
func (ec EventContextV03) Validate() ValidationError {
errors := map[string]error{}
// type
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the organization which defines the semantics of this event type.
eventType := strings.TrimSpace(ec.Type)
if eventType == "" {
errors["type"] = fmt.Errorf("MUST be a non-empty string")
}
// source
// Type: URI-reference
// Constraints:
// REQUIRED
source := strings.TrimSpace(ec.Source.String())
if source == "" {
errors["source"] = fmt.Errorf("REQUIRED")
}
// subject
// Type: String
// Constraints:
// OPTIONAL
// MUST be a non-empty string
if ec.Subject != nil {
subject := strings.TrimSpace(*ec.Subject)
if subject == "" {
errors["subject"] = fmt.Errorf("if present, MUST be a non-empty string")
}
}
// id
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// MUST be unique within the scope of the producer
id := strings.TrimSpace(ec.ID)
if id == "" {
errors["id"] = fmt.Errorf("MUST be a non-empty string")
// no way to test "MUST be unique within the scope of the producer"
}
// time
// Type: Timestamp
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3339
// --> no need to test this, no way to set the time without it being valid.
// schemaurl
// Type: URI
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3986
if ec.SchemaURL != nil {
schemaURL := strings.TrimSpace(ec.SchemaURL.String())
// empty string is not RFC 3986 compatible.
if schemaURL == "" {
errors["schemaurl"] = fmt.Errorf("if present, MUST adhere to the format specified in RFC 3986")
}
}
// datacontenttype
// Type: String per RFC 2046
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 2046
if ec.DataContentType != nil {
dataContentType := strings.TrimSpace(*ec.DataContentType)
if dataContentType == "" {
errors["datacontenttype"] = fmt.Errorf("if present, MUST adhere to the format specified in RFC 2046")
} else {
_, _, err := mime.ParseMediaType(dataContentType)
if err != nil {
errors["datacontenttype"] = fmt.Errorf("if present, MUST adhere to the format specified in RFC 2046")
}
}
}
// datacontentencoding
// Type: String per RFC 2045 Section 6.1
// Constraints:
// The attribute MUST be set if the data attribute contains string-encoded binary data.
// Otherwise the attribute MUST NOT be set.
// If present, MUST adhere to RFC 2045 Section 6.1
if ec.DataContentEncoding != nil {
dataContentEncoding := strings.ToLower(strings.TrimSpace(*ec.DataContentEncoding))
if dataContentEncoding != Base64 {
errors["datacontentencoding"] = fmt.Errorf("if present, MUST adhere to RFC 2045 Section 6.1")
}
}
if len(errors) > 0 {
return errors
}
return nil
}
// String returns a pretty-printed representation of the EventContext.
func (ec EventContextV03) String() string {
b := strings.Builder{}
b.WriteString("Context Attributes,\n")
b.WriteString(" specversion: " + CloudEventsVersionV03 + "\n")
b.WriteString(" type: " + ec.Type + "\n")
b.WriteString(" source: " + ec.Source.String() + "\n")
if ec.Subject != nil {
b.WriteString(" subject: " + *ec.Subject + "\n")
}
b.WriteString(" id: " + ec.ID + "\n")
if ec.Time != nil {
b.WriteString(" time: " + ec.Time.String() + "\n")
}
if ec.SchemaURL != nil {
b.WriteString(" schemaurl: " + ec.SchemaURL.String() + "\n")
}
if ec.DataContentType != nil {
b.WriteString(" datacontenttype: " + *ec.DataContentType + "\n")
}
if ec.DataContentEncoding != nil {
b.WriteString(" datacontentencoding: " + *ec.DataContentEncoding + "\n")
}
if ec.Extensions != nil && len(ec.Extensions) > 0 {
b.WriteString("Extensions,\n")
keys := make([]string, 0, len(ec.Extensions))
for k := range ec.Extensions {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString(fmt.Sprintf(" %s: %v\n", key, ec.Extensions[key]))
}
}
return b.String()
}

View File

@ -0,0 +1,99 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"fmt"
"strings"
"time"
)
// GetSpecVersion implements EventContextReader.GetSpecVersion
func (ec EventContextV03) GetSpecVersion() string {
return CloudEventsVersionV03
}
// GetDataContentType implements EventContextReader.GetDataContentType
func (ec EventContextV03) GetDataContentType() string {
if ec.DataContentType != nil {
return *ec.DataContentType
}
return ""
}
// GetDataMediaType implements EventContextReader.GetDataMediaType
func (ec EventContextV03) GetDataMediaType() (string, error) {
if ec.DataContentType != nil {
dct := *ec.DataContentType
i := strings.IndexRune(dct, ';')
if i == -1 {
return dct, nil
}
return strings.TrimSpace(dct[0:i]), nil
}
return "", nil
}
// GetType implements EventContextReader.GetType
func (ec EventContextV03) GetType() string {
return ec.Type
}
// GetSource implements EventContextReader.GetSource
func (ec EventContextV03) GetSource() string {
return ec.Source.String()
}
// GetSubject implements EventContextReader.GetSubject
func (ec EventContextV03) GetSubject() string {
if ec.Subject != nil {
return *ec.Subject
}
return ""
}
// GetTime implements EventContextReader.GetTime
func (ec EventContextV03) GetTime() time.Time {
if ec.Time != nil {
return ec.Time.Time
}
return time.Time{}
}
// GetID implements EventContextReader.GetID
func (ec EventContextV03) GetID() string {
return ec.ID
}
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV03) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
return ""
}
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV03) DeprecatedGetDataContentEncoding() string {
if ec.DataContentEncoding != nil {
return *ec.DataContentEncoding
}
return ""
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV03) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV03) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package event
import (
"errors"
"net/url"
"strings"
"time"
"github.com/cloudevents/sdk-go/v2/types"
)
// Adhere to EventContextWriter
var _ EventContextWriter = (*EventContextV03)(nil)
// SetDataContentType implements EventContextWriter.SetDataContentType
func (ec *EventContextV03) SetDataContentType(ct string) error {
ct = strings.TrimSpace(ct)
if ct == "" {
ec.DataContentType = nil
} else {
ec.DataContentType = &ct
}
return nil
}
// SetType implements EventContextWriter.SetType
func (ec *EventContextV03) SetType(t string) error {
t = strings.TrimSpace(t)
ec.Type = t
return nil
}
// SetSource implements EventContextWriter.SetSource
func (ec *EventContextV03) SetSource(u string) error {
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.Source = types.URIRef{URL: *pu}
return nil
}
// SetSubject implements EventContextWriter.SetSubject
func (ec *EventContextV03) SetSubject(s string) error {
s = strings.TrimSpace(s)
if s == "" {
ec.Subject = nil
} else {
ec.Subject = &s
}
return nil
}
// SetID implements EventContextWriter.SetID
func (ec *EventContextV03) SetID(id string) error {
id = strings.TrimSpace(id)
if id == "" {
return errors.New("id is required to be a non-empty string")
}
ec.ID = id
return nil
}
// SetTime implements EventContextWriter.SetTime
func (ec *EventContextV03) SetTime(t time.Time) error {
if t.IsZero() {
ec.Time = nil
} else {
ec.Time = &types.Timestamp{Time: t}
}
return nil
}
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV03) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
return nil
}
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.SchemaURL = &types.URIRef{URL: *pu}
return nil
}
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV03) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
ec.DataContentEncoding = nil
} else {
ec.DataContentEncoding = &e
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More