From e07e9bab0dc8a81af91ddd170d1b076e9af727cd Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Fri, 15 Nov 2019 23:06:02 -0800 Subject: [PATCH] This adds a simple cloudevents-go sample. (#1987) This example is intended to showcase how to start an event receiver as a Knative Service, and then have that service either send a new event to a `$K_SINK` or respond with it (e.g. for working with a Broker). Once we're happy with this, we should broaden this to cover more languages. --- Gopkg.lock | 16 +- docs/serving/samples/cloudevents/_index.md | 6 + .../cloudevents/cloudevents-go/Dockerfile | 31 ++ .../cloudevents/cloudevents-go/README.md | 120 ++++++ .../cloudevents/cloudevents-go/cloudevents.go | 105 +++++ .../cloudevents/cloudevents-go/service.yaml | 14 + vendor/github.com/cloudevents/sdk-go/alias.go | 8 + .../sdk-go/pkg/cloudevents/datacodec/codec.go | 3 + .../pkg/cloudevents/datacodec/text/text.go | 33 ++ .../sdk-go/pkg/cloudevents/event.go | 13 +- .../sdk-go/pkg/cloudevents/event_data.go | 46 ++- .../sdk-go/pkg/cloudevents/event_interface.go | 19 +- .../sdk-go/pkg/cloudevents/event_marshal.go | 119 +++++- .../sdk-go/pkg/cloudevents/event_reader.go | 14 +- .../sdk-go/pkg/cloudevents/event_writer.go | 18 +- .../sdk-go/pkg/cloudevents/eventcontext.go | 46 ++- .../pkg/cloudevents/eventcontext_v01.go | 8 +- .../cloudevents/eventcontext_v01_reader.go | 19 +- .../cloudevents/eventcontext_v01_writer.go | 8 +- .../pkg/cloudevents/eventcontext_v02.go | 7 +- .../cloudevents/eventcontext_v02_reader.go | 19 +- .../cloudevents/eventcontext_v02_writer.go | 8 +- .../pkg/cloudevents/eventcontext_v03.go | 47 ++- .../cloudevents/eventcontext_v03_reader.go | 19 +- .../cloudevents/eventcontext_v03_writer.go | 8 +- .../sdk-go/pkg/cloudevents/eventcontext_v1.go | 300 ++++++++++++++ .../pkg/cloudevents/eventcontext_v1_reader.go | 98 +++++ .../pkg/cloudevents/eventcontext_v1_writer.go | 102 +++++ .../sdk-go/pkg/cloudevents/extensions.go | 21 +- .../sdk-go/pkg/cloudevents/transport/error.go | 9 +- .../pkg/cloudevents/transport/http/codec.go | 184 ++++----- .../transport/http/codec_structured.go | 2 +- .../cloudevents/transport/http/codec_v01.go | 51 ++- .../cloudevents/transport/http/codec_v02.go | 39 +- .../cloudevents/transport/http/codec_v03.go | 41 +- .../cloudevents/transport/http/codec_v1.go | 245 +++++++++++ .../cloudevents/transport/http/encoding.go | 71 +++- .../pkg/cloudevents/transport/http/options.go | 2 +- .../cloudevents/transport/http/transport.go | 38 +- .../sdk-go/pkg/cloudevents/types/doc.go | 39 +- .../sdk-go/pkg/cloudevents/types/timestamp.go | 43 +- .../sdk-go/pkg/cloudevents/types/uri.go | 77 ++++ .../sdk-go/pkg/cloudevents/types/uriref.go | 77 ++++ .../sdk-go/pkg/cloudevents/types/urlref.go | 2 + .../sdk-go/pkg/cloudevents/types/value.go | 260 ++++++++++++ .../kelseyhightower/envconfig/LICENSE | 19 + .../kelseyhightower/envconfig/doc.go | 8 + .../kelseyhightower/envconfig/env_os.go | 7 + .../kelseyhightower/envconfig/env_syscall.go | 7 + .../kelseyhightower/envconfig/envconfig.go | 382 ++++++++++++++++++ .../kelseyhightower/envconfig/usage.go | 164 ++++++++ 51 files changed, 2742 insertions(+), 300 deletions(-) create mode 100644 docs/serving/samples/cloudevents/_index.md create mode 100644 docs/serving/samples/cloudevents/cloudevents-go/Dockerfile create mode 100644 docs/serving/samples/cloudevents/cloudevents-go/README.md create mode 100644 docs/serving/samples/cloudevents/cloudevents-go/cloudevents.go create mode 100644 docs/serving/samples/cloudevents/cloudevents-go/service.yaml create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text/text.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_reader.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_writer.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v1.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uri.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uriref.go create mode 100644 vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/value.go create mode 100644 vendor/github.com/kelseyhightower/envconfig/LICENSE create mode 100644 vendor/github.com/kelseyhightower/envconfig/doc.go create mode 100644 vendor/github.com/kelseyhightower/envconfig/env_os.go create mode 100644 vendor/github.com/kelseyhightower/envconfig/env_syscall.go create mode 100644 vendor/github.com/kelseyhightower/envconfig/envconfig.go create mode 100644 vendor/github.com/kelseyhightower/envconfig/usage.go diff --git a/Gopkg.lock b/Gopkg.lock index e7254cb1a..6b684ab1a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -26,7 +26,7 @@ revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] - digest = "1:d3c3de7c1ad57f795e5c409afa842fea27899f124622bcdce83f8bea8721b9db" + digest = "1:902544577dcb868a5ae31529d73a1ce5031e224923290caedf6176241ee304e0" name = "github.com/cloudevents/sdk-go" packages = [ ".", @@ -35,6 +35,7 @@ "pkg/cloudevents/context", "pkg/cloudevents/datacodec", "pkg/cloudevents/datacodec/json", + "pkg/cloudevents/datacodec/text", "pkg/cloudevents/datacodec/xml", "pkg/cloudevents/observability", "pkg/cloudevents/transport", @@ -42,8 +43,8 @@ "pkg/cloudevents/types", ] pruneopts = "NUT" - revision = "4cc108a637ff4bf2d1848c60d5fbb0f711fd1b8c" - version = "v0.9.2" + revision = "2fa4bb1fbb4aac4d906b0173a2a408f701439b82" + version = "v0.10.0" [[projects]] digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2" @@ -142,6 +143,14 @@ revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" version = "v1.6.2" +[[projects]] + digest = "1:08c58ac78a8c1f61e9a96350066d30fe194b8779799bd932a79932a5166a173f" + name = "github.com/kelseyhightower/envconfig" + packages = ["."] + pruneopts = "NUT" + revision = "0b417c4ec4a8a82eecc22a1459a504aa55163d61" + version = "v1.4.0" + [[projects]] digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" @@ -477,6 +486,7 @@ "github.com/google/go-github/github", "github.com/google/uuid", "github.com/gorilla/mux", + "github.com/kelseyhightower/envconfig", "github.com/openzipkin/zipkin-go", "github.com/openzipkin/zipkin-go/reporter/http", "github.com/satori/go.uuid", diff --git a/docs/serving/samples/cloudevents/_index.md b/docs/serving/samples/cloudevents/_index.md new file mode 100644 index 000000000..3b3cbf099 --- /dev/null +++ b/docs/serving/samples/cloudevents/_index.md @@ -0,0 +1,6 @@ +--- +title: "Knative Serving 'Cloud Events' samples" +linkTitle: "Cloud Events apps" +weight: 1 +type: "docs" +--- diff --git a/docs/serving/samples/cloudevents/cloudevents-go/Dockerfile b/docs/serving/samples/cloudevents/cloudevents-go/Dockerfile new file mode 100644 index 000000000..e141bf2cc --- /dev/null +++ b/docs/serving/samples/cloudevents/cloudevents-go/Dockerfile @@ -0,0 +1,31 @@ +# Use the official Golang image to create a build artifact. +# This is based on Debian and sets the GOPATH to /go. +# https://hub.docker.com/_/golang +FROM golang:1.13 as builder + +# Create and change to the app directory. +WORKDIR /app + +# Retrieve application dependencies using go modules. +# Allows container builds to reuse downloaded dependencies. +COPY go.* ./ +RUN go mod download + +# Copy local code to the container image. +COPY . ./ + +# Build the binary. +# -mod=readonly ensures immutable go.mod and go.sum in container builds. +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o server + +# Use the official Alpine image for a lean production container. +# https://hub.docker.com/_/alpine +# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds +FROM alpine:3 +RUN apk add --no-cache ca-certificates + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /app/server /server + +# Run the web service on container startup. +CMD ["/server"] diff --git a/docs/serving/samples/cloudevents/cloudevents-go/README.md b/docs/serving/samples/cloudevents/cloudevents-go/README.md new file mode 100644 index 000000000..63d72fc9f --- /dev/null +++ b/docs/serving/samples/cloudevents/cloudevents-go/README.md @@ -0,0 +1,120 @@ +--- +title: "Cloud Events - Go" +linkTitle: "Go" +weight: 1 +type: "docs" +--- + +A simple web app written in Go that can receive and send Cloud Events that you +can use for testing. It supports running in two modes: +1. The default mode has the app reply to your input events with the output + event, which is simplest for demonstrating things working in isolation, + but is also the model for working for the Knative Eventing `Broker` concept. + +2. `K_SINK` mode has the app send events to the destination encoded in `$K_SINK`, + which is useful to demonstrate how folks can synthesize events to send to + a Service or Broker when not initiated by a Broker invocation (e.g. + implementing an event source) + +The application will use `$K_SINK`-mode whenever the environment variable is specified. + +Follow the steps below to create the sample code and then deploy the app to your +cluster. You can also download a working copy of the sample, by running the +following commands: + +```shell +git clone -b "{{< branch >}}" https://github.com/knative/docs knative-docs +cd knative-docs/docs/serving/samples/cloudevents/cloudevents-go +``` + +## Before you begin + +- A Kubernetes cluster with Knative installed and DNS configured. Follow the + [installation instructions](../../../../install/README.md) if you need to + create one. +- [Docker](https://www.docker.com) installed and running on your local machine, + and a Docker Hub account configured (we'll use it for a container registry). + +## The sample code. + +1. If you look in `cloudevents.go`, you will see two key functions for the + different modes of operation: + + ```go + func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error { + // This is called whenever an event is received if $K_SINK is set, and sends a new event + // to the url in $K_SINK. + } + + func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error { + // This is called whenever an event is received if $K_SINK is NOT set, and it replies with + // the new event instead. + } + ``` + +1. If you look in `Dockerfile`, you will see a method for pulling in the dependencies and + building a small Go container based on Alpine. You can build and push this to your + registry of choice via: + + ```shell + docker build -t . + docker push + ``` + + Alternatively you can use [`ko`](https://github.com/google/ko) to build and + push just the image with: + + ```shell + ko publish github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go + ``` + +1. If you look in `service.yaml`, take the `` name above and insert it + into the `image:` field (unless using `ko`). + + ```shell + kubectl apply -f service.yaml + ``` + + Or if using `ko` to build and push: + + ```shell + ko apply -f service.yaml + ``` + +## Testing the sample + +Get the URL for your Service with: + +```shell +$ kubectl get ksvc +NAME URL LATESTCREATED LATESTREADY READY REASON +cloudevents-go http://cloudevents-go.default.1.2.3.4.xip.io cloudevents-go-ss5pj cloudevents-go-ss5pj True +``` + +Then send a cloud event to it with: + +```shell +$ curl -X POST \ + -H "content-type: application/json" \ + -H "ce-specversion: 1.0" \ + -H "ce-source: curl-command" \ + -H "ce-type: curl.demo" \ + -H "ce-id: 123-abc" \ + -d '{"name":"Dave"}' \ + http://cloudevents-go.default.1.2.3.4.xip.io +``` + +You will get back: + +```shell +{"message":"Hello, Dave"} +``` + + +## Removing the sample app deployment + +To remove the sample app from your cluster, delete the service record: + +```shell +kubectl delete --filename service.yaml +``` diff --git a/docs/serving/samples/cloudevents/cloudevents-go/cloudevents.go b/docs/serving/samples/cloudevents/cloudevents-go/cloudevents.go new file mode 100644 index 000000000..74ecd1db3 --- /dev/null +++ b/docs/serving/samples/cloudevents/cloudevents-go/cloudevents.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + cloudevents "github.com/cloudevents/sdk-go" + "github.com/kelseyhightower/envconfig" +) + +type Receiver struct { + client cloudevents.Client + + // If the K_SINK environment variable is set, then events are sent there, + // otherwise we simply reply to the inbound request. + Target string `envconfig:"K_SINK"` +} + +func main() { + client, err := cloudevents.NewDefaultClient() + if err != nil { + log.Fatal(err.Error()) + } + + r := Receiver{client: client} + if err := envconfig.Process("", &r); err != nil { + log.Fatal(err.Error()) + } + + // Depending on whether targetting data has been supplied, + // we will either reply with our response or send it on to + // an event sink. + var receiver interface{} // the SDK reflects on the signature. + if r.Target == "" { + receiver = r.ReceiveAndReply + } else { + receiver = r.ReceiveAndSend + } + + if err := client.StartReceiver(context.Background(), receiver); err != nil { + log.Fatal(err) + } +} + +// Request is the structure of the event we expect to receive. +type Request struct { + Name string `json:"name"` +} + +// Response is the structure of the event we send in response to requests. +type Response struct { + Message string `json:"message,omitempty"` +} + +// handle shared the logic for producing the Response event from the Request. +func handle(req Request) (resp Response) { + resp.Message = fmt.Sprintf("Hello, %s", req.Name) + return +} + +// ReceiveAndSend is invoked whenever we receive an event. +func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error { + req := Request{} + if err := event.DataAs(&req); err != nil { + return err + } + log.Printf("Got an event from: %q", req.Name) + + resp := handle(req) + log.Printf("Sending event: %q", resp.Message) + + r := cloudevents.NewEvent(cloudevents.VersionV1) + r.SetType("dev.knative.docs.sample") + r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go") + r.SetDataContentType("application/json") + r.SetData(resp) + + ctx = cloudevents.ContextWithTarget(ctx, recv.Target) + _, _, err := recv.client.Send(ctx, event) + return err +} + +// ReceiveAndReply is invoked whenever we receive an event. +func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error { + req := Request{} + if err := event.DataAs(&req); err != nil { + return err + } + log.Printf("Got an event from: %q", req.Name) + + resp := handle(req) + log.Printf("Replying with event: %q", resp.Message) + + r := cloudevents.NewEvent(cloudevents.VersionV1) + r.SetType("dev.knative.docs.sample") + r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go") + r.SetDataContentType("application/json") + r.SetData(resp) + + eventResp.RespondWith(http.StatusOK, &r) + + return nil +} diff --git a/docs/serving/samples/cloudevents/cloudevents-go/service.yaml b/docs/serving/samples/cloudevents/cloudevents-go/service.yaml new file mode 100644 index 000000000..31a85ddbf --- /dev/null +++ b/docs/serving/samples/cloudevents/cloudevents-go/service.yaml @@ -0,0 +1,14 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: cloudevents-go + namespace: default +spec: + template: + spec: + containers: + - image: github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go + # Uncomment this to send events somewhere. + # env: + # - name: K_SINK + # value: http://default-broker.default.svc.cluster.local diff --git a/vendor/github.com/cloudevents/sdk-go/alias.go b/vendor/github.com/cloudevents/sdk-go/alias.go index f97b6473a..92f5b40c7 100644 --- a/vendor/github.com/cloudevents/sdk-go/alias.go +++ b/vendor/github.com/cloudevents/sdk-go/alias.go @@ -26,6 +26,7 @@ type EventResponse = cloudevents.EventResponse // Context type EventContext = cloudevents.EventContext +type EventContextV1 = cloudevents.EventContextV1 type EventContextV01 = cloudevents.EventContextV01 type EventContextV02 = cloudevents.EventContextV02 type EventContextV03 = cloudevents.EventContextV03 @@ -54,12 +55,16 @@ const ( // Event Versions + VersionV1 = cloudevents.CloudEventsVersionV1 VersionV01 = cloudevents.CloudEventsVersionV01 VersionV02 = cloudevents.CloudEventsVersionV02 VersionV03 = cloudevents.CloudEventsVersionV03 // HTTP Transport Encodings + HTTPBinaryV1 = http.BinaryV1 + HTTPStructuredV1 = http.StructuredV1 + HTTPBatchedV1 = http.BatchedV1 HTTPBinaryV01 = http.BinaryV01 HTTPStructuredV01 = http.StructuredV01 HTTPBinaryV02 = http.BinaryV02 @@ -114,6 +119,8 @@ var ( ParseTimestamp = types.ParseTimestamp ParseURLRef = types.ParseURLRef + ParseURIRef = types.ParseURIRef + ParseURI = types.ParseURI // HTTP Transport @@ -133,6 +140,7 @@ var ( WithPath = http.WithPath WithMiddleware = http.WithMiddleware WithLongPollTarget = http.WithLongPollTarget + WithListener = http.WithListener // HTTP Context diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/codec.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/codec.go index 41425c21f..b9674889c 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/codec.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/codec.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/json" + "github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text" "github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/xml" "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" ) @@ -30,12 +31,14 @@ func init() { 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 diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text/text.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text/text.go new file mode 100644 index 000000000..3c37c5b13 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text/text.go @@ -0,0 +1,33 @@ +// Text codec converts []byte or string to string and vice-versa. +package text + +import ( + "context" + "fmt" +) + +func Decode(_ context.Context, in, out interface{}) error { + p, _ := out.(*string) + if p == nil { + return fmt.Errorf("text.Decode out: want *string, got %T", out) + } + switch s := in.(type) { + case string: + *p = s + case []byte: + *p = string(s) + case nil: // treat nil like []byte{} + *p = "" + default: + return fmt.Errorf("text.Decode in: want []byte or string, got %T", 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 +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event.go index 9b0b90367..e00b9dce3 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event.go @@ -12,6 +12,7 @@ type Event struct { Context EventContext Data interface{} DataEncoded bool + DataBinary bool } const ( @@ -30,7 +31,17 @@ func New(version ...string) Event { return *e } -// ExtensionAs returns 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"]) +// func (e Event) ExtensionAs(name string, obj interface{}) error { return e.Context.ExtensionAs(name, obj) } diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_data.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_data.go index 9e4cb9d24..26ce70ea3 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_data.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_data.go @@ -14,11 +14,43 @@ import ( // SetData implements EventWriter.SetData func (e *Event) SetData(obj interface{}) error { + if e.SpecVersion() != CloudEventsVersionV1 { + return e.legacySetData(obj) + } + + // Version 1.0 and above. + + // TODO: we will have to be smarter about how data relates to media type. + // but the issue is we can not just encode data anymore without understanding + // what the encoding will be on the outbound event. Structured will use + // data_base64, binary will not (if the transport supports binary mode). + + // TODO: look at content encoding too. + + switch obj.(type) { + case []byte: + e.Data = obj + e.DataEncoded = true + e.DataBinary = true + default: + data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj) + if err != nil { + return err + } + e.Data = data + e.DataEncoded = true + e.DataBinary = false + } + + return nil +} + +func (e *Event) legacySetData(obj interface{}) error { data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj) if err != nil { return err } - if e.DataContentEncoding() == Base64 { + if e.DeprecatedDataContentEncoding() == Base64 { buf := make([]byte, base64.StdEncoding.EncodedLen(len(data))) base64.StdEncoding.Encode(buf, data) e.Data = string(buf) @@ -70,7 +102,7 @@ func (e Event) DataAs(data interface{}) error { // TODO: Clean this function up // No data. return nil } - if e.Context.GetDataContentEncoding() == Base64 { + if e.Context.DeprecatedGetDataContentEncoding() == Base64 { var bs []byte // test to see if we need to unquote the data. if obj[0] == quotes[0] || obj[0] == quotes[1] { @@ -91,9 +123,13 @@ func (e Event) DataAs(data interface{}) error { // TODO: Clean this function up obj = buf[:n] } - mediaType, err := e.Context.GetDataMediaType() - if err != nil { - return err + mediaType := "" + if e.Context.GetDataContentType() != "" { + var err error + mediaType, err = e.Context.GetDataMediaType() + if err != nil { + return err + } } return datacodec.Decode(context.Background(), mediaType, obj, data) } diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_interface.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_interface.go index 8ca52b04d..1249f25dd 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_interface.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_interface.go @@ -18,26 +18,29 @@ type EventReader interface { ID() string // Time returns event.Context.GetTime(). Time() time.Time - // SchemaURL returns event.Context.GetSchemaURL(). - SchemaURL() string + // DataSchema returns event.Context.GetDataSchema(). + DataSchema() string // DataContentType returns event.Context.GetDataContentType(). DataContentType() string // DataMediaType returns event.Context.GetDataMediaType(). DataMediaType() string - // DataContentEncoding returns event.Context.GetDataContentEncoding(). - DataContentEncoding() 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{} + // DEPRECATED: see event.Context.ExtensionAs // ExtensionAs returns event.Context.ExtensionAs(name, obj). ExtensionAs(string, interface{}) error // Data Attribute - // ExtensionAs returns event.Context.ExtensionAs(name, obj). + // DataAs attempts to populate the provided data object with the event payload. + // data should be a pointer type. DataAs(interface{}) error } @@ -58,11 +61,11 @@ type EventWriter interface { SetID(string) // SetTime performs event.Context.SetTime. SetTime(time.Time) - // SetSchemaURL performs event.Context.SetSchemaURL. - SetSchemaURL(string) + // SetDataSchema performs event.Context.SetDataSchema. + SetDataSchema(string) // SetDataContentType performs event.Context.SetDataContentType. SetDataContentType(string) - // SetDataContentEncoding performs event.Context.SetDataContentEncoding. + // DeprecatedSetDataContentEncoding performs event.Context.DeprecatedSetDataContentEncoding. SetDataContentEncoding(string) // Extension Attributes diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_marshal.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_marshal.go index 5e2c1602e..f7c864f59 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_marshal.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_marshal.go @@ -2,9 +2,12 @@ package cloudevents import ( "context" + "encoding/base64" "encoding/json" + "errors" "fmt" "strconv" + "strings" "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" ) @@ -19,7 +22,17 @@ func (e Event) MarshalJSON() ([]byte, error) { return nil, err } - b, err := JsonEncode(e) + var b []byte + var err error + + switch e.SpecVersion() { + case CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03: + b, err = JsonEncodeLegacy(e) + case CloudEventsVersionV1: + b, err = JsonEncode(e) + default: + return nil, fmt.Errorf("unnknown spec version: %q", e.SpecVersion()) + } // Report the observable if err != nil { @@ -52,6 +65,8 @@ func (e *Event) UnmarshalJSON(b []byte) error { err = e.JsonDecodeV02(b, raw) case CloudEventsVersionV03: err = e.JsonDecodeV03(b, raw) + case CloudEventsVersionV1: + err = e.JsonDecodeV1(b, raw) default: return fmt.Errorf("unnknown spec version: %q", version) } @@ -89,17 +104,26 @@ func versionFromRawMessage(raw map[string]json.RawMessage) string { // JsonEncode func JsonEncode(e Event) ([]byte, error) { - if e.DataContentType() == "" { - e.SetDataContentType(ApplicationJSON) - } data, err := e.DataBytes() if err != nil { return nil, err } - return jsonEncode(e.Context, data) + return jsonEncode(e.Context, data, e.DataBinary) } -func jsonEncode(ctx EventContextReader, data []byte) ([]byte, error) { +// JsonEncodeLegacy +func JsonEncodeLegacy(e Event) ([]byte, error) { + var data []byte + isBase64 := e.Context.DeprecatedGetDataContentEncoding() == Base64 + var err error + data, err = e.DataBytes() + if err != nil { + return nil, err + } + return jsonEncode(e.Context, data, isBase64) +} + +func jsonEncode(ctx EventContextReader, data []byte, isBase64 bool) ([]byte, error) { var b map[string]json.RawMessage var err error @@ -122,16 +146,26 @@ func jsonEncode(ctx EventContextReader, data []byte) ([]byte, error) { if err != nil { return nil, err } - isBase64 := ctx.GetDataContentEncoding() == Base64 isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON // TODO(#60): we do not support json values at the moment, only objects and lists. if isJson && !isBase64 { b["data"] = data - } else if data[0] != byte('"') { - b["data"] = []byte(strconv.QuoteToASCII(string(data))) } else { - // already quoted - b["data"] = data + var dataKey string + if ctx.GetSpecVersion() == CloudEventsVersionV1 { + dataKey = "data_base64" + buf := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(buf, data) + data = buf + } else { + dataKey = "data" + } + if data[0] != byte('"') { + b[dataKey] = []byte(strconv.QuoteToASCII(string(data))) + } else { + // already quoted + b[dataKey] = data + } } } @@ -189,6 +223,7 @@ func (e *Event) JsonDecodeV02(body []byte, raw map[string]json.RawMessage) error if len(raw) > 0 { extensions := make(map[string]interface{}, len(raw)) for k, v := range raw { + k = strings.ToLower(k) extensions[k] = v } ec.Extensions = extensions @@ -229,6 +264,7 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error if len(raw) > 0 { extensions := make(map[string]interface{}, len(raw)) for k, v := range raw { + k = strings.ToLower(k) extensions[k] = v } ec.Extensions = extensions @@ -241,6 +277,66 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error return nil } +// JsonDecodeV1 takes in the byte representation of a version 1.0 structured json CloudEvent and returns a +// cloudevent.Event or an error if there are parsing errors. +func (e *Event) JsonDecodeV1(body []byte, raw map[string]json.RawMessage) error { + ec := EventContextV1{} + if err := json.Unmarshal(body, &ec); err != nil { + return err + } + + delete(raw, "specversion") + delete(raw, "type") + delete(raw, "source") + delete(raw, "subject") + delete(raw, "id") + delete(raw, "time") + delete(raw, "dataschema") + delete(raw, "datacontenttype") + + var data interface{} + if d, ok := raw["data"]; ok { + data = []byte(d) + } + delete(raw, "data") + + var dataBase64 []byte + if d, ok := raw["data_base64"]; ok { + var tmp []byte + if err := json.Unmarshal(d, &tmp); err != nil { + return err + } + dataBase64 = tmp + } + delete(raw, "data_base64") + + if len(raw) > 0 { + extensions := make(map[string]interface{}, len(raw)) + for k, v := range raw { + k = strings.ToLower(k) + var tmp string + if err := json.Unmarshal(v, &tmp); err != nil { + return err + } + extensions[k] = tmp + } + ec.Extensions = extensions + } + + e.Context = &ec + if data != nil && dataBase64 != nil { + return errors.New("parsing error: JSON decoder found both 'data', and 'data_base64' in JSON payload") + } + if data != nil { + e.Data = data + } else if dataBase64 != nil { + e.Data = dataBase64 + } + e.DataEncoded = data != nil + + return nil +} + func marshalEventLegacy(event interface{}) (map[string]json.RawMessage, error) { b, err := json.Marshal(event) if err != nil { @@ -267,6 +363,7 @@ func marshalEvent(event interface{}, extensions map[string]interface{}) (map[str } for k, v := range extensions { + k = strings.ToLower(k) vb, err := json.Marshal(v) if err != nil { return nil, err diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_reader.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_reader.go index a5be4ecf8..fe49e8424 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_reader.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_reader.go @@ -54,10 +54,10 @@ func (e Event) Time() time.Time { return time.Time{} } -// SchemaURL implements EventReader.SchemaURL -func (e Event) SchemaURL() string { +// DataSchema implements EventReader.DataSchema +func (e Event) DataSchema() string { if e.Context != nil { - return e.Context.GetSchemaURL() + return e.Context.GetDataSchema() } return "" } @@ -81,15 +81,15 @@ func (e Event) DataMediaType() string { return "" } -// DataContentEncoding implements EventReader.DataContentEncoding -func (e Event) DataContentEncoding() string { +// DeprecatedDataContentEncoding implements EventReader.DeprecatedDataContentEncoding +func (e Event) DeprecatedDataContentEncoding() string { if e.Context != nil { - return e.Context.GetDataContentEncoding() + return e.Context.DeprecatedGetDataContentEncoding() } return "" } -// DataContentEncoding implements EventReader.DataContentEncoding +// Extensions implements EventReader.Extensions func (e Event) Extensions() map[string]interface{} { if e.Context != nil { return e.Context.GetExtensions() diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_writer.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_writer.go index ce5b3e876..4f6f0f80e 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_writer.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/event_writer.go @@ -17,9 +17,11 @@ func (e *Event) SetSpecVersion(v string) { e.Context = EventContextV02{}.AsV02() case CloudEventsVersionV03: e.Context = EventContextV03{}.AsV03() + case CloudEventsVersionV1: + e.Context = EventContextV1{}.AsV1() default: - panic(fmt.Errorf("a valid spec version is required: [%s, %s, %s]", - CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03)) + panic(fmt.Errorf("a valid spec version is required: [%s, %s, %s, %s]", + CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03, CloudEventsVersionV1)) } return } @@ -63,9 +65,9 @@ func (e *Event) SetTime(t time.Time) { } } -// SetSchemaURL implements EventWriter.SetSchemaURL -func (e *Event) SetSchemaURL(s string) { - if err := e.Context.SetSchemaURL(s); err != nil { +// SetDataSchema implements EventWriter.SetDataSchema +func (e *Event) SetDataSchema(s string) { + if err := e.Context.SetDataSchema(s); err != nil { panic(err) } } @@ -77,14 +79,14 @@ func (e *Event) SetDataContentType(ct string) { } } -// SetDataContentEncoding implements EventWriter.SetDataContentEncoding +// DeprecatedSetDataContentEncoding implements EventWriter.DeprecatedSetDataContentEncoding func (e *Event) SetDataContentEncoding(enc string) { - if err := e.Context.SetDataContentEncoding(enc); err != nil { + if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil { panic(err) } } -// SetDataContentEncoding implements EventWriter.SetDataContentEncoding +// SetExtension implements EventWriter.SetExtension func (e *Event) SetExtension(name string, obj interface{}) { if err := e.Context.SetExtension(name, obj); err != nil { panic(err) diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext.go index 92ad1f729..a0309e495 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext.go @@ -18,27 +18,40 @@ type EventContextReader interface { GetID() string // GetTime returns the CloudEvents creation time from the context. GetTime() time.Time - // GetSchemaURL returns the CloudEvents schema URL (if any) from the + // GetDataSchema returns the CloudEvents schema URL (if any) from the // context. - GetSchemaURL() string + GetDataSchema() string // GetDataContentType returns content type on the context. GetDataContentType() string - // GetDataContentEncoding returns content encoding on the context. - GetDataContentEncoding() 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) - // ExtensionAs populates the given interface with the CloudEvents extension - // of the given name from the extension attributes. It returns an error if - // the extension does not exist, the extension's type does not match the - // provided type, or if the type is not a supported. + // 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 @@ -56,18 +69,22 @@ type EventContextWriter interface { SetID(string) error // SetTime sets the time of the context. SetTime(time time.Time) error - // SetSchemaURL sets the schema url of the context. - SetSchemaURL(string) error + // SetDataSchema sets the schema url of the context. + SetDataSchema(string) error // SetDataContentType sets the data content type of the context. SetDataContentType(string) error - // SetDataContentEncoding sets the data context encoding of the context. - SetDataContentEncoding(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. + // + // 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 { // AsV01 provides a translation from whatever the "native" encoding of the // CloudEvent was to the equivalent in v0.1 field names, moving fields to or @@ -83,6 +100,11 @@ type EventContextConverter interface { // 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. diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01.go index d4f416dd1..0b01823be 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01.go @@ -115,8 +115,12 @@ func (ec EventContextV01) AsV02() *EventContextV02 { // AsV03 implements EventContextConverter.AsV03 func (ec EventContextV01) AsV03() *EventContextV03 { - ecv2 := ec.AsV02() - return ecv2.AsV03() + return ec.AsV02().AsV03() +} + +// AsV1 implements EventContextConverter.AsV1 +func (ec EventContextV01) AsV1() *EventContextV1 { + return ec.AsV02().AsV03().AsV1() } // Validate returns errors based on requirements from the CloudEvents spec. diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_reader.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_reader.go index 12d46863a..8d75ea70c 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_reader.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_reader.go @@ -1,6 +1,7 @@ package cloudevents import ( + "fmt" "mime" "time" ) @@ -68,16 +69,16 @@ func (ec EventContextV01) GetTime() time.Time { return time.Time{} } -// GetSchemaURL implements EventContextReader.GetSchemaURL -func (ec EventContextV01) GetSchemaURL() string { +// GetDataSchema implements EventContextReader.GetDataSchema +func (ec EventContextV01) GetDataSchema() string { if ec.SchemaURL != nil { return ec.SchemaURL.String() } return "" } -// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding -func (ec EventContextV01) GetDataContentEncoding() string { +// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding +func (ec EventContextV01) DeprecatedGetDataContentEncoding() string { var enc string if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil { return "" @@ -85,6 +86,16 @@ func (ec EventContextV01) GetDataContentEncoding() string { return enc } +// GetExtensions implements EventContextReader.GetExtensions func (ec EventContextV01) GetExtensions() map[string]interface{} { return ec.Extensions } + +// GetExtension implements EventContextReader.GetExtension +func (ec EventContextV01) GetExtension(key string) (interface{}, error) { + v, ok := caseInsensitiveSearch(key, ec.Extensions) + if !ok { + return "", fmt.Errorf("%q not found", key) + } + return v, nil +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_writer.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_writer.go index 7c196d939..e49d3cca7 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_writer.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v01_writer.go @@ -79,8 +79,8 @@ func (ec *EventContextV01) SetTime(t time.Time) error { return nil } -// SetSchemaURL implements EventContextWriter.SetSchemaURL -func (ec *EventContextV01) SetSchemaURL(u string) error { +// SetDataSchema implements EventContextWriter.SetDataSchema +func (ec *EventContextV01) SetDataSchema(u string) error { u = strings.TrimSpace(u) if u == "" { ec.SchemaURL = nil @@ -94,8 +94,8 @@ func (ec *EventContextV01) SetSchemaURL(u string) error { return nil } -// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding -func (ec *EventContextV01) SetDataContentEncoding(e string) error { +// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding +func (ec *EventContextV01) DeprecatedSetDataContentEncoding(e string) error { e = strings.ToLower(strings.TrimSpace(e)) if e == "" { return ec.SetExtension(DataContentEncodingKey, nil) diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02.go index ed4affc38..3dde8a19b 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02.go @@ -146,7 +146,7 @@ func (ec EventContextV02) AsV03() *EventContextV03 { } continue } - // DataContentEncoding was introduced in 0.3 + // DeprecatedDataContentEncoding was introduced in 0.3 if strings.EqualFold(k, DataContentEncodingKey) { etv, ok := v.(string) if ok && etv != "" { @@ -163,6 +163,11 @@ func (ec EventContextV02) AsV03() *EventContextV03 { return &ret } +// AsV1 implements EventContextConverter.AsV1 +func (ec EventContextV02) AsV1() *EventContextV1 { + return ec.AsV03().AsV1() +} + // Validate returns errors based on requirements from the CloudEvents spec. // For more details, see https://github.com/cloudevents/spec/blob/v0.2/spec.md func (ec EventContextV02) Validate() error { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_reader.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_reader.go index 224066e98..120cdb87e 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_reader.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_reader.go @@ -1,6 +1,7 @@ package cloudevents import ( + "fmt" "mime" "time" ) @@ -48,8 +49,8 @@ func (ec EventContextV02) GetTime() time.Time { return time.Time{} } -// GetSchemaURL implements EventContextReader.GetSchemaURL -func (ec EventContextV02) GetSchemaURL() string { +// GetDataSchema implements EventContextReader.GetDataSchema +func (ec EventContextV02) GetDataSchema() string { if ec.SchemaURL != nil { return ec.SchemaURL.String() } @@ -76,8 +77,8 @@ func (ec EventContextV02) GetDataMediaType() (string, error) { return "", nil } -// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding -func (ec EventContextV02) GetDataContentEncoding() string { +// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding +func (ec EventContextV02) DeprecatedGetDataContentEncoding() string { var enc string if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil { return "" @@ -85,6 +86,16 @@ func (ec EventContextV02) GetDataContentEncoding() string { return enc } +// GetExtensions implements EventContextReader.GetExtensions func (ec EventContextV02) GetExtensions() map[string]interface{} { return ec.Extensions } + +// GetExtension implements EventContextReader.GetExtension +func (ec EventContextV02) GetExtension(key string) (interface{}, error) { + v, ok := caseInsensitiveSearch(key, ec.Extensions) + if !ok { + return "", fmt.Errorf("%q not found", key) + } + return v, nil +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_writer.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_writer.go index 8935e93d7..25b8a16c8 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_writer.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v02_writer.go @@ -79,8 +79,8 @@ func (ec *EventContextV02) SetTime(t time.Time) error { return nil } -// SetSchemaURL implements EventContextWriter.SetSchemaURL -func (ec *EventContextV02) SetSchemaURL(u string) error { +// SetDataSchema implements EventContextWriter.SetDataSchema +func (ec *EventContextV02) SetDataSchema(u string) error { u = strings.TrimSpace(u) if u == "" { ec.SchemaURL = nil @@ -94,8 +94,8 @@ func (ec *EventContextV02) SetSchemaURL(u string) error { return nil } -// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding -func (ec *EventContextV02) SetDataContentEncoding(e string) error { +// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding +func (ec *EventContextV02) DeprecatedSetDataContentEncoding(e string) error { e = strings.ToLower(strings.TrimSpace(e)) if e == "" { return ec.SetExtension(DataContentEncodingKey, nil) diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03.go index 5f97c043e..2e714d187 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03.go @@ -9,8 +9,6 @@ import ( "github.com/cloudevents/sdk-go/pkg/cloudevents/types" ) -// WIP: AS OF FEB 19, 2019 - const ( // CloudEventsVersionV03 represents the version 0.3 of the CloudEvents spec. CloudEventsVersionV03 = "0.3" @@ -32,12 +30,12 @@ type EventContextV03 struct { ID string `json:"id"` // Time - A Timestamp when the event happened. Time *types.Timestamp `json:"time,omitempty"` - // SchemaURL - A link to the schema that the `data` attribute adheres to. + // DataSchema - A link to the schema that the `data` attribute adheres to. SchemaURL *types.URLRef `json:"schemaurl,omitempty"` // GetDataMediaType - A MIME (RFC2046) string describing the media type of `data`. // TODO: Should an empty string assume `application/json`, `application/octet-stream`, or auto-detect the content? DataContentType *string `json:"datacontenttype,omitempty"` - // DataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`. + // 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:"-"` @@ -85,7 +83,11 @@ func (ec *EventContextV03) SetExtension(name string, value interface{}) error { if value == nil { delete(ec.Extensions, name) } else { - ec.Extensions[name] = value + v, err := types.Validate(value) + if err == nil { + ec.Extensions[name] = v + } + return err } return nil } @@ -117,7 +119,7 @@ func (ec EventContextV03) AsV02() *EventContextV02 { if ec.Subject != nil { _ = ret.SetExtension(SubjectKey, *ec.Subject) } - // DataContentEncoding was introduced in 0.3, so put it in an extension for 0.2. + // DeprecatedDataContentEncoding was introduced in 0.3, so put it in an extension for 0.2. if ec.DataContentEncoding != nil { _ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding) } @@ -138,6 +140,39 @@ func (ec EventContextV03) AsV03() *EventContextV03 { return &ec } +// AsV04 implements EventContextConverter.AsV04 +func (ec EventContextV03) AsV1() *EventContextV1 { + ret := EventContextV1{ + SpecVersion: CloudEventsVersionV1, + 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 diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_reader.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_reader.go index e4d998508..2b3cc207f 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_reader.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_reader.go @@ -1,6 +1,7 @@ package cloudevents import ( + "fmt" "mime" "time" ) @@ -64,22 +65,32 @@ func (ec EventContextV03) GetID() string { return ec.ID } -// GetSchemaURL implements EventContextReader.GetSchemaURL -func (ec EventContextV03) GetSchemaURL() string { +// GetDataSchema implements EventContextReader.GetDataSchema +func (ec EventContextV03) GetDataSchema() string { if ec.SchemaURL != nil { return ec.SchemaURL.String() } return "" } -// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding -func (ec EventContextV03) GetDataContentEncoding() string { +// 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 +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_writer.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_writer.go index 9370d2a3d..0c1bb8428 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_writer.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v03_writer.go @@ -81,8 +81,8 @@ func (ec *EventContextV03) SetTime(t time.Time) error { return nil } -// SetSchemaURL implements EventContextWriter.SetSchemaURL -func (ec *EventContextV03) SetSchemaURL(u string) error { +// SetDataSchema implements EventContextWriter.SetDataSchema +func (ec *EventContextV03) SetDataSchema(u string) error { u = strings.TrimSpace(u) if u == "" { ec.SchemaURL = nil @@ -96,8 +96,8 @@ func (ec *EventContextV03) SetSchemaURL(u string) error { return nil } -// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding -func (ec *EventContextV03) SetDataContentEncoding(e string) error { +// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding +func (ec *EventContextV03) DeprecatedSetDataContentEncoding(e string) error { e = strings.ToLower(strings.TrimSpace(e)) if e == "" { ec.DataContentEncoding = nil diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1.go new file mode 100644 index 000000000..c7bda117a --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1.go @@ -0,0 +1,300 @@ +package cloudevents + +import ( + "errors" + "fmt" + "mime" + "sort" + "strings" + + "github.com/cloudevents/sdk-go/pkg/cloudevents/types" +) + +// WIP: AS OF SEP 20, 2019 + +const ( + // CloudEventsVersionV1 represents the version 1.0 of the CloudEvents spec. + CloudEventsVersionV1 = "1.0" +) + +// EventContextV1 represents the non-data attributes of a CloudEvents v1.0 +// event. +type EventContextV1 struct { + // ID of the event; must be non-empty and unique within the scope of the producer. + // +required + ID string `json:"id"` + // Source - A URI describing the event producer. + // +required + Source types.URIRef `json:"source"` + // SpecVersion - The version of the CloudEvents specification used by the event. + // +required + SpecVersion string `json:"specversion"` + // Type - The type of the occurrence which has happened. + // +required + Type string `json:"type"` + + // DataContentType - A MIME (RFC2046) string describing the media type of `data`. + // +optional + DataContentType *string `json:"datacontenttype,omitempty"` + // Subject - The subject of the event in the context of the event producer + // (identified by `source`). + // +optional + Subject *string `json:"subject,omitempty"` + // Time - A Timestamp when the event happened. + // +optional + Time *types.Timestamp `json:"time,omitempty"` + // DataSchema - A link to the schema that the `data` attribute adheres to. + // +optional + DataSchema *types.URI `json:"dataschema,omitempty"` + + // Extensions - Additional extension metadata beyond the base spec. + // +optional + Extensions map[string]interface{} `json:"-"` +} + +// Adhere to EventContext +var _ EventContext = (*EventContextV1)(nil) + +// ExtensionAs implements EventContext.ExtensionAs +func (ec EventContextV1) ExtensionAs(name string, obj interface{}) error { + name = strings.ToLower(name) + value, ok := ec.Extensions[name] + if !ok { + return fmt.Errorf("extension %q does not exist", name) + } + + // Only support *string for now. + if v, ok := obj.(*string); ok { + if *v, ok = value.(string); ok { + return nil + } + } + return fmt.Errorf("unknown extension type %T", obj) +} + +// SetExtension adds the extension 'name' with value 'value' to the CloudEvents context. +func (ec *EventContextV1) SetExtension(name string, value interface{}) error { + if !IsAlphaNumericLowercaseLetters(name) { + return errors.New("bad key, CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z') or digits ('0' to '9') from the ASCII character set") + } + + name = strings.ToLower(name) + if ec.Extensions == nil { + ec.Extensions = make(map[string]interface{}) + } + if value == nil { + delete(ec.Extensions, name) + return nil + } else { + v, err := types.Validate(value) // Ensure it's a legal CE attribute value + if err == nil { + ec.Extensions[name] = v + } + return err + } +} + +// Clone implements EventContextConverter.Clone +func (ec EventContextV1) Clone() EventContext { + return ec.AsV1() +} + +// AsV01 implements EventContextConverter.AsV01 +func (ec EventContextV1) AsV01() *EventContextV01 { + ecv2 := ec.AsV02() + return ecv2.AsV01() +} + +// AsV02 implements EventContextConverter.AsV02 +func (ec EventContextV1) AsV02() *EventContextV02 { + ecv3 := ec.AsV03() + return ecv3.AsV02() +} + +// AsV03 implements EventContextConverter.AsV03 +func (ec EventContextV1) AsV03() *EventContextV03 { + ret := EventContextV03{ + SpecVersion: CloudEventsVersionV03, + ID: ec.ID, + Time: ec.Time, + Type: ec.Type, + DataContentType: ec.DataContentType, + Source: types.URLRef{URL: ec.Source.URL}, + Subject: ec.Subject, + Extensions: make(map[string]interface{}), + } + + if ec.DataSchema != nil { + ret.SchemaURL = &types.URLRef{URL: ec.DataSchema.URL} + } + + // TODO: DeprecatedDataContentEncoding needs to be moved to extensions. + if ec.Extensions != nil { + for k, v := range ec.Extensions { + k = strings.ToLower(k) + // DeprecatedDataContentEncoding was introduced in 0.3, removed in 1.0 + if strings.EqualFold(k, DataContentEncodingKey) { + etv, ok := v.(string) + if ok && etv != "" { + ret.DataContentEncoding = &etv + } + continue + } + ret.Extensions[k] = v + } + } + if len(ret.Extensions) == 0 { + ret.Extensions = nil + } + return &ret +} + +// AsV04 implements EventContextConverter.AsV04 +func (ec EventContextV1) AsV1() *EventContextV1 { + ec.SpecVersion = CloudEventsVersionV1 + return &ec +} + +// Validate returns errors based on requirements from the CloudEvents spec. +// For more details, see https://github.com/cloudevents/spec/blob/v1.0-rc1/spec.md. +func (ec EventContextV1) Validate() error { + errors := []string(nil) + + // 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 = append(errors, "id: MUST be a non-empty string") + // no way to test "MUST be unique within the scope of the producer" + } + + // source + // Type: URI-reference + // Constraints: + // REQUIRED + // MUST be a non-empty URI-reference + // An absolute URI is RECOMMENDED + source := strings.TrimSpace(ec.Source.String()) + if source == "" { + errors = append(errors, "source: REQUIRED") + } + + // specversion + // Type: String + // Constraints: + // REQUIRED + // MUST be a non-empty string + specVersion := strings.TrimSpace(ec.SpecVersion) + if specVersion == "" { + errors = append(errors, "specversion: MUST be a non-empty string") + } + + // 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 = append(errors, "type: MUST be a non-empty string") + } + + // The following attributes are optional but still have validation. + + // 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 = append(errors, "datacontenttype: if present, MUST adhere to the format specified in RFC 2046") + } else { + _, _, err := mime.ParseMediaType(dataContentType) + if err != nil { + errors = append(errors, fmt.Sprintf("datacontenttype: failed to parse media type, %s", err.Error())) + } + } + } + + // dataschema + // Type: URI + // Constraints: + // OPTIONAL + // If present, MUST adhere to the format specified in RFC 3986 + if ec.DataSchema != nil { + dataSchema := strings.TrimSpace(ec.DataSchema.String()) + // empty string is not RFC 3986 compatible. + if dataSchema == "" { + errors = append(errors, "dataschema: if present, MUST adhere to the format specified in RFC 3986") + } + } + + // subject + // Type: String + // Constraints: + // OPTIONAL + // MUST be a non-empty string + if ec.Subject != nil { + subject := strings.TrimSpace(*ec.Subject) + if subject == "" { + errors = append(errors, "subject: if present, MUST be a non-empty string") + } + } + + // 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. + + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, "\n")) + } + return nil +} + +// String returns a pretty-printed representation of the EventContext. +func (ec EventContextV1) String() string { + b := strings.Builder{} + + b.WriteString("Context Attributes,\n") + + b.WriteString(" specversion: " + ec.SpecVersion + "\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.DataSchema != nil { + b.WriteString(" dataschema: " + ec.DataSchema.String() + "\n") + } + if ec.DataContentType != nil { + b.WriteString(" datacontenttype: " + *ec.DataContentType + "\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() +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_reader.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_reader.go new file mode 100644 index 000000000..e3f329d31 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_reader.go @@ -0,0 +1,98 @@ +package cloudevents + +import ( + "fmt" + "mime" + "time" +) + +// GetSpecVersion implements EventContextReader.GetSpecVersion +func (ec EventContextV1) GetSpecVersion() string { + if ec.SpecVersion != "" { + return ec.SpecVersion + } + return CloudEventsVersionV03 +} + +// GetDataContentType implements EventContextReader.GetDataContentType +func (ec EventContextV1) GetDataContentType() string { + if ec.DataContentType != nil { + return *ec.DataContentType + } + return "" +} + +// GetDataMediaType implements EventContextReader.GetDataMediaType +func (ec EventContextV1) GetDataMediaType() (string, error) { + if ec.DataContentType != nil { + mediaType, _, err := mime.ParseMediaType(*ec.DataContentType) + if err != nil { + return "", err + } + return mediaType, nil + } + return "", nil +} + +// GetType implements EventContextReader.GetType +func (ec EventContextV1) GetType() string { + return ec.Type +} + +// GetSource implements EventContextReader.GetSource +func (ec EventContextV1) GetSource() string { + return ec.Source.String() +} + +// GetSubject implements EventContextReader.GetSubject +func (ec EventContextV1) GetSubject() string { + if ec.Subject != nil { + return *ec.Subject + } + return "" +} + +// GetTime implements EventContextReader.GetTime +func (ec EventContextV1) GetTime() time.Time { + if ec.Time != nil { + return ec.Time.Time + } + return time.Time{} +} + +// GetID implements EventContextReader.GetID +func (ec EventContextV1) GetID() string { + return ec.ID +} + +// GetDataSchema implements EventContextReader.GetDataSchema +func (ec EventContextV1) GetDataSchema() string { + if ec.DataSchema != nil { + return ec.DataSchema.String() + } + return "" +} + +// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding +func (ec EventContextV1) DeprecatedGetDataContentEncoding() string { + return "" +} + +// GetExtensions implements EventContextReader.GetExtensions +func (ec EventContextV1) GetExtensions() map[string]interface{} { + // For now, convert the extensions of v1.0 to the pre-v1.0 style. + ext := make(map[string]interface{}, len(ec.Extensions)) + for k, v := range ec.Extensions { + ext[k] = v + } + return ext +} + +// GetExtension implements EventContextReader.GetExtension +func (ec EventContextV1) GetExtension(key string) (interface{}, error) { + v, ok := caseInsensitiveSearch(key, ec.Extensions) + if !ok { + return "", fmt.Errorf("%q not found", key) + } + return v, nil +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_writer.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_writer.go new file mode 100644 index 000000000..dc33ba2f6 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/eventcontext_v1_writer.go @@ -0,0 +1,102 @@ +package cloudevents + +import ( + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/cloudevents/sdk-go/pkg/cloudevents/types" +) + +// Adhere to EventContextWriter +var _ EventContextWriter = (*EventContextV1)(nil) + +// SetSpecVersion implements EventContextWriter.SetSpecVersion +func (ec *EventContextV1) SetSpecVersion(v string) error { + if v != CloudEventsVersionV1 { + return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV1) + } + ec.SpecVersion = CloudEventsVersionV1 + return nil +} + +// SetDataContentType implements EventContextWriter.SetDataContentType +func (ec *EventContextV1) 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 *EventContextV1) SetType(t string) error { + t = strings.TrimSpace(t) + ec.Type = t + return nil +} + +// SetSource implements EventContextWriter.SetSource +func (ec *EventContextV1) 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 *EventContextV1) 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 *EventContextV1) 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 *EventContextV1) 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 *EventContextV1) SetDataSchema(u string) error { + u = strings.TrimSpace(u) + if u == "" { + ec.DataSchema = nil + return nil + } + pu, err := url.Parse(u) + if err != nil { + return err + } + ec.DataSchema = &types.URI{URL: *pu} + return nil +} + +// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding +func (ec *EventContextV1) DeprecatedSetDataContentEncoding(e string) error { + return errors.New("deprecated: SetDataContentEncoding is not supported in v1.0 of CloudEvents") +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/extensions.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/extensions.go index e33205fc8..c8ba54846 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/extensions.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/extensions.go @@ -1,13 +1,30 @@ package cloudevents +import ( + "regexp" + "strings" +) + const ( - // DataContentEncodingKey is the key to DataContentEncoding for versions that do not support data content encoding + // DataContentEncodingKey is the key to DeprecatedDataContentEncoding for versions that do not support data content encoding // directly. DataContentEncodingKey = "datacontentencoding" // EventTypeVersionKey is the key to EventTypeVersion for versions that do not support event type version directly. - EventTypeVersionKey = "eventTypeVersion" + EventTypeVersionKey = "eventtypeversion" // SubjectKey is the key to Subject for versions that do not support subject directly. SubjectKey = "subject" ) + +func caseInsensitiveSearch(key string, space map[string]interface{}) (interface{}, bool) { + lkey := strings.ToLower(key) + for k, v := range space { + if strings.EqualFold(lkey, strings.ToLower(k)) { + return v, true + } + } + return nil, false +} + +var IsAlphaNumericLowercaseLetters = regexp.MustCompile(`^[a-z0-9]+$`).MatchString diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/error.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/error.go index 95e0f342e..bb4e8ec9f 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/error.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/error.go @@ -6,15 +6,17 @@ import "fmt" // message can not be converted. type ErrTransportMessageConversion struct { fatal bool + handled bool transport string message string } // NewErrMessageEncodingUnknown makes a new ErrMessageEncodingUnknown. -func NewErrTransportMessageConversion(transport, message string, fatal bool) *ErrTransportMessageConversion { +func NewErrTransportMessageConversion(transport, message string, handled, fatal bool) *ErrTransportMessageConversion { return &ErrTransportMessageConversion{ transport: transport, message: message, + handled: handled, fatal: fatal, } } @@ -24,6 +26,11 @@ func (e *ErrTransportMessageConversion) IsFatal() bool { return e.fatal } +// Handled reports if this error should be considered accepted and no further action. +func (e *ErrTransportMessageConversion) Handled() bool { + return e.handled +} + // Error implements error.Error func (e *ErrTransportMessageConversion) Error() string { return fmt.Sprintf("transport %s failed to convert message: %s", e.transport, e.message) diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec.go index 6774111e5..9dcceda0b 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec.go @@ -2,9 +2,12 @@ package http import ( "context" + "errors" "fmt" + "sync" "github.com/cloudevents/sdk-go/pkg/cloudevents" + cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context" "github.com/cloudevents/sdk-go/pkg/cloudevents/transport" ) @@ -21,152 +24,127 @@ type Codec struct { v01 *CodecV01 v02 *CodecV02 v03 *CodecV03 + v1 *CodecV1 + + _v01 sync.Once + _v02 sync.Once + _v03 sync.Once + _v1 sync.Once } // Adheres to Codec var _ transport.Codec = (*Codec)(nil) -// Encode encodes the provided event into a transport message. -func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - encoding := c.Encoding - - if encoding == Default && c.DefaultEncodingSelectionFn != nil { - encoding = c.DefaultEncodingSelectionFn(ctx, e) - } - +func (c *Codec) loadCodec(encoding Encoding) (transport.Codec, error) { switch encoding { case Default: fallthrough - case BinaryV01: - fallthrough - case StructuredV01: - if c.v01 == nil { - c.v01 = &CodecV01{Encoding: encoding} - } - return c.v01.Encode(ctx, e) - case BinaryV02: - fallthrough - case StructuredV02: - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: encoding} - } - return c.v02.Encode(ctx, e) - case BinaryV03: - fallthrough - case StructuredV03: - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: encoding} - } - return c.v03.Encode(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %s", encoding) + case BinaryV01, StructuredV01: + c._v01.Do(func() { + c.v01 = &CodecV01{DefaultEncoding: c.Encoding} + }) + return c.v01, nil + case BinaryV02, StructuredV02: + c._v02.Do(func() { + c.v02 = &CodecV02{DefaultEncoding: c.Encoding} + }) + return c.v02, nil + case BinaryV03, StructuredV03, BatchedV03: + c._v03.Do(func() { + c.v03 = &CodecV03{DefaultEncoding: c.Encoding} + }) + return c.v03, nil + case BinaryV1, StructuredV1, BatchedV1: + c._v1.Do(func() { + c.v1 = &CodecV1{DefaultEncoding: c.Encoding} + }) + return c.v1, nil } + return nil, fmt.Errorf("unknown encoding: %s", encoding) +} + +// Encode encodes the provided event into a transport message. +func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { + encoding := c.Encoding + if encoding == Default && c.DefaultEncodingSelectionFn != nil { + encoding = c.DefaultEncodingSelectionFn(ctx, e) + } + codec, err := c.loadCodec(encoding) + if err != nil { + return nil, err + } + ctx = cecontext.WithEncoding(ctx, encoding.Name()) + return codec.Encode(ctx, e) } // Decode converts a provided transport message into an Event, or error. func (c *Codec) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - switch c.inspectEncoding(ctx, msg) { - case BinaryV01, StructuredV01: - if c.v01 == nil { - c.v01 = &CodecV01{Encoding: c.Encoding} - } - if event, err := c.v01.Decode(ctx, msg); err != nil { - return nil, err - } else { - return c.convertEvent(event), nil - } - - case BinaryV02, StructuredV02: - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} - } - if event, err := c.v02.Decode(ctx, msg); err != nil { - return nil, err - } else { - return c.convertEvent(event), nil - } - - case BinaryV03, StructuredV03, BatchedV03: - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } - if event, err := c.v03.Decode(ctx, msg); err != nil { - return nil, err - } else { - return c.convertEvent(event), nil - } - default: - return nil, transport.NewErrMessageEncodingUnknown("wrapper", TransportName) + codec, err := c.loadCodec(c.inspectEncoding(ctx, msg)) + if err != nil { + return nil, err } + event, err := codec.Decode(ctx, msg) + if err != nil { + return nil, err + } + return c.convertEvent(event) } // Give the context back as the user expects -func (c *Codec) convertEvent(event *cloudevents.Event) *cloudevents.Event { +func (c *Codec) convertEvent(event *cloudevents.Event) (*cloudevents.Event, error) { if event == nil { - return nil + return nil, errors.New("event is nil, can not convert") } + switch c.Encoding { case Default: - return event - case BinaryV01: - fallthrough - case StructuredV01: - if c.v01 == nil { - c.v01 = &CodecV01{Encoding: c.Encoding} - } + return event, nil + case BinaryV01, StructuredV01: ca := event.Context.AsV01() event.Context = ca - return event - case BinaryV02: - fallthrough - case StructuredV02: - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} - } + return event, nil + case BinaryV02, StructuredV02: ca := event.Context.AsV02() event.Context = ca - return event - case BinaryV03: - fallthrough - case StructuredV03: - fallthrough - case BatchedV03: - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } + return event, nil + case BinaryV03, StructuredV03, BatchedV03: ca := event.Context.AsV03() event.Context = ca - return event + return event, nil + case BinaryV1, StructuredV1, BatchedV1: + ca := event.Context.AsV03() + event.Context = ca + return event, nil default: - return nil + return nil, fmt.Errorf("unknown encoding: %s", c.Encoding) } } func (c *Codec) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { - // TODO: there should be a better way to make the version codecs on demand. - if c.v01 == nil { - c.v01 = &CodecV01{Encoding: c.Encoding} - } - // Try v0.1 first. - encoding := c.v01.inspectEncoding(ctx, msg) + // Try v1.0. + _, _ = c.loadCodec(BinaryV1) + encoding := c.v1.inspectEncoding(ctx, msg) if encoding != Unknown { return encoding } - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} + // Try v0.3. + _, _ = c.loadCodec(BinaryV03) + encoding = c.v03.inspectEncoding(ctx, msg) + if encoding != Unknown { + return encoding } + // Try v0.2. + _, _ = c.loadCodec(BinaryV02) encoding = c.v02.inspectEncoding(ctx, msg) if encoding != Unknown { return encoding } - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } - // Try v0.3. - encoding = c.v03.inspectEncoding(ctx, msg) + // Try v0.1 first. + _, _ = c.loadCodec(BinaryV01) + encoding = c.v01.inspectEncoding(ctx, msg) if encoding != Unknown { return encoding } diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_structured.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_structured.go index 098cb5a15..d67d7186e 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_structured.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_structured.go @@ -13,7 +13,7 @@ import ( // CodecStructured represents an structured http transport codec for all versions. // Intended to be used as a base class. type CodecStructured struct { - Encoding Encoding + DefaultEncoding Encoding } func (v CodecStructured) encodeStructured(ctx context.Context, e cloudevents.Event) (transport.Message, error) { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v01.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v01.go index e414c090a..435ba93fb 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v01.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v01.go @@ -9,16 +9,17 @@ import ( "strings" "github.com/cloudevents/sdk-go/pkg/cloudevents" + cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context" "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/pkg/cloudevents/transport" "github.com/cloudevents/sdk-go/pkg/cloudevents/types" ) -// CodecV01 represents a http transport codec that uses CloudEvents spec v0.3 +// CodecV01 represents a http transport codec that uses CloudEvents spec v0.1 type CodecV01 struct { CodecStructured - Encoding Encoding + DefaultEncoding Encoding } // Adheres to Codec @@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV01)(nil) // Encode implements Codec.Encode func (v CodecV01) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - // TODO: wire context - _, r := observability.NewReporter(context.Background(), CodecObserved{o: reportEncode, c: v.Encoding.Codec()}) - m, err := v.obsEncode(ctx, e) + encoding := v.DefaultEncoding + strEnc := cecontext.EncodingFrom(ctx) + if strEnc != "" { + switch strEnc { + case Binary: + encoding = BinaryV01 + case Structured: + encoding = StructuredV01 + } + } + + _, r := observability.NewReporter(context.Background(), CodecObserved{o: reportEncode, c: encoding.Codec()}) + m, err := v.obsEncode(ctx, e, encoding) if err != nil { r.Error() } else { @@ -37,8 +48,8 @@ func (v CodecV01) Encode(ctx context.Context, e cloudevents.Event) (transport.Me return m, err } -func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { +func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) { + switch encoding { case Default: fallthrough case BinaryV01: @@ -46,13 +57,12 @@ func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event) (transport case StructuredV01: return v.encodeStructured(ctx, e) default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) + return nil, fmt.Errorf("unknown encoding: %d", encoding) } } // Decode implements Codec.Decode func (v CodecV01) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // TODO: wire context _, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free. e, err := v.obsDecode(ctx, msg) if err != nil { @@ -107,15 +117,10 @@ func (v CodecV01) toHeaders(ec *cloudevents.EventContextV01) (http.Header, error h["CE-EventTypeVersion"] = []string{*ec.EventTypeVersion} } if ec.SchemaURL != nil { - h["CE-SchemaURL"] = []string{ec.SchemaURL.String()} + h["CE-DataSchema"] = []string{ec.SchemaURL.String()} } - if ec.ContentType != nil { + if ec.ContentType != nil && *ec.ContentType != "" { h.Set("Content-Type", *ec.ContentType) - } else if v.Encoding == Default || v.Encoding == BinaryV01 { - // in binary v0.1, the Content-Type header is tied to ec.ContentType - // This was later found to be an issue with the spec, but yolo. - // TODO: not sure what the default should be? - h.Set("Content-Type", cloudevents.ApplicationJSON) } // Regarding Extensions, v0.1 Spec says the following: @@ -172,17 +177,23 @@ func (v CodecV01) fromHeaders(h http.Header) (cloudevents.EventContextV01, error if source != nil { ec.Source = *source } - ec.EventTime = types.ParseTimestamp(h.Get("CE-EventTime")) + var err error + ec.EventTime, err = types.ParseTimestamp(h.Get("CE-EventTime")) + if err != nil { + return ec, err + } h.Del("CE-EventTime") etv := h.Get("CE-EventTypeVersion") h.Del("CE-EventTypeVersion") if etv != "" { ec.EventTypeVersion = &etv } - ec.SchemaURL = types.ParseURLRef(h.Get("CE-SchemaURL")) - h.Del("CE-SchemaURL") + ec.SchemaURL = types.ParseURLRef(h.Get("CE-DataSchema")) + h.Del("CE-DataSchema") et := h.Get("Content-Type") - ec.ContentType = &et + if et != "" { + ec.ContentType = &et + } extensions := make(map[string]interface{}) for k, v := range h { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v02.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v02.go index 939e60204..aeb67c0e8 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v02.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v02.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/cloudevents/sdk-go/pkg/cloudevents" + cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context" "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/pkg/cloudevents/transport" "github.com/cloudevents/sdk-go/pkg/cloudevents/types" @@ -18,7 +19,7 @@ import ( type CodecV02 struct { CodecStructured - Encoding Encoding + DefaultEncoding Encoding } // Adheres to Codec @@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV02)(nil) // Encode implements Codec.Encode func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - // TODO: wire context - _, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: v.Encoding.Codec()}) - m, err := v.obsEncode(ctx, e) + encoding := v.DefaultEncoding + strEnc := cecontext.EncodingFrom(ctx) + if strEnc != "" { + switch strEnc { + case Binary: + encoding = BinaryV02 + case Structured: + encoding = StructuredV02 + } + } + + _, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()}) + m, err := v.obsEncode(ctx, e, encoding) if err != nil { r.Error() } else { @@ -37,8 +48,8 @@ func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Me return m, err } -func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { +func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) { + switch encoding { case Default: fallthrough case BinaryV02: @@ -46,13 +57,12 @@ func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event) (transport case StructuredV02: return v.encodeStructured(ctx, e) default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) + return nil, fmt.Errorf("unknown encoding: %d", encoding) } } // Decode implements Codec.Decode func (v CodecV02) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // TODO: wire context _, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free. e, err := v.obsDecode(ctx, msg) if err != nil { @@ -104,13 +114,8 @@ func (v CodecV02) toHeaders(ec *cloudevents.EventContextV02) (http.Header, error if ec.SchemaURL != nil { h.Set("ce-schemaurl", ec.SchemaURL.String()) } - if ec.ContentType != nil { + if ec.ContentType != nil && *ec.ContentType != "" { h.Set("Content-Type", *ec.ContentType) - } else if v.Encoding == Default || v.Encoding == BinaryV02 { - // in binary v0.2, the Content-Type header is tied to ec.ContentType - // This was later found to be an issue with the spec, but yolo. - // TODO: not sure what the default should be? - h.Set("Content-Type", cloudevents.ApplicationJSON) } for k, v := range ec.Extensions { // Per spec, map-valued extensions are converted to a list of headers as: @@ -182,7 +187,11 @@ func (v CodecV02) fromHeaders(h http.Header) (cloudevents.EventContextV02, error } h.Del("ce-source") - ec.Time = types.ParseTimestamp(h.Get("ce-time")) + var err error + ec.Time, err = types.ParseTimestamp(h.Get("ce-time")) + if err != nil { + return ec, err + } h.Del("ce-time") ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl")) diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v03.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v03.go index b1236bf81..b2b3c87ee 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v03.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v03.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/cloudevents/sdk-go/pkg/cloudevents" + cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context" "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/pkg/cloudevents/transport" "github.com/cloudevents/sdk-go/pkg/cloudevents/types" @@ -18,7 +19,7 @@ import ( type CodecV03 struct { CodecStructured - Encoding Encoding + DefaultEncoding Encoding } // Adheres to Codec @@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV03)(nil) // Encode implements Codec.Encode func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - // TODO: wire context - _, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: v.Encoding.Codec()}) - m, err := v.obsEncode(ctx, e) + encoding := v.DefaultEncoding + strEnc := cecontext.EncodingFrom(ctx) + if strEnc != "" { + switch strEnc { + case Binary: + encoding = BinaryV03 + case Structured: + encoding = StructuredV03 + } + } + + _, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()}) + m, err := v.obsEncode(ctx, e, encoding) if err != nil { r.Error() } else { @@ -37,8 +48,8 @@ func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Me return m, err } -func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { +func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) { + switch encoding { case Default: fallthrough case BinaryV03: @@ -48,13 +59,12 @@ func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event) (transport case BatchedV03: return nil, fmt.Errorf("not implemented") default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) + return nil, fmt.Errorf("unknown encoding: %d", encoding) } } // Decode implements Codec.Decode func (v CodecV03) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // TODO: wire context _, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free. e, err := v.obsDecode(ctx, msg) if err != nil { @@ -112,19 +122,15 @@ func (v CodecV03) toHeaders(ec *cloudevents.EventContextV03) (http.Header, error if ec.SchemaURL != nil { h.Set("ce-schemaurl", ec.SchemaURL.String()) } - if ec.DataContentType != nil { + if ec.DataContentType != nil && *ec.DataContentType != "" { h.Set("Content-Type", *ec.DataContentType) - } else if v.Encoding == Default || v.Encoding == BinaryV03 { - // in binary v0.2, the Content-Type header is tied to ec.ContentType - // This was later found to be an issue with the spec, but yolo. - // TODO: not sure what the default should be? - h.Set("Content-Type", cloudevents.ApplicationJSON) } if ec.DataContentEncoding != nil { h.Set("ce-datacontentencoding", *ec.DataContentEncoding) } for k, v := range ec.Extensions { + k = strings.ToLower(k) // Per spec, map-valued extensions are converted to a list of headers as: // CE-attrib-key switch v.(type) { @@ -212,7 +218,11 @@ func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error } h.Del("ce-subject") - ec.Time = types.ParseTimestamp(h.Get("ce-time")) + var err error + ec.Time, err = types.ParseTimestamp(h.Get("ce-time")) + if err != nil { + return ec, err + } h.Del("ce-time") ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl")) @@ -235,6 +245,7 @@ func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error extensions := make(map[string]interface{}) for k, v := range h { + k = strings.ToLower(k) if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") { ak := strings.ToLower(k[len("ce-"):]) if i := strings.Index(ak, "-"); i > 0 { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v1.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v1.go new file mode 100644 index 000000000..4ebe7422b --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/codec_v1.go @@ -0,0 +1,245 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "net/textproto" + "strings" + + "github.com/cloudevents/sdk-go/pkg/cloudevents" + cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context" + "github.com/cloudevents/sdk-go/pkg/cloudevents/observability" + "github.com/cloudevents/sdk-go/pkg/cloudevents/transport" + "github.com/cloudevents/sdk-go/pkg/cloudevents/types" +) + +// CodecV1 represents a http transport codec that uses CloudEvents spec v1.0 +type CodecV1 struct { + CodecStructured + + DefaultEncoding Encoding +} + +// Adheres to Codec +var _ transport.Codec = (*CodecV1)(nil) + +// Encode implements Codec.Encode +func (v CodecV1) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { + encoding := v.DefaultEncoding + strEnc := cecontext.EncodingFrom(ctx) + if strEnc != "" { + switch strEnc { + case Binary: + encoding = BinaryV1 + case Structured: + encoding = StructuredV1 + } + } + + _, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()}) + m, err := v.obsEncode(ctx, e, encoding) + if err != nil { + r.Error() + } else { + r.OK() + } + return m, err +} + +func (v CodecV1) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) { + switch encoding { + case Default: + fallthrough + case BinaryV1: + return v.encodeBinary(ctx, e) + case StructuredV1: + return v.encodeStructured(ctx, e) + case BatchedV1: + return nil, fmt.Errorf("not implemented") + default: + return nil, fmt.Errorf("unknown encoding: %d", encoding) + } +} + +// Decode implements Codec.Decode +func (v CodecV1) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { + _, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free. + e, err := v.obsDecode(ctx, msg) + if err != nil { + r.Error() + } else { + r.OK() + } + return e, err +} + +func (v CodecV1) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { + switch v.inspectEncoding(ctx, msg) { + case BinaryV1: + return v.decodeBinary(ctx, msg) + case StructuredV1: + return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV1, msg) + case BatchedV1: + return nil, fmt.Errorf("not implemented") + default: + return nil, transport.NewErrMessageEncodingUnknown("V1", TransportName) + } +} + +func (v CodecV1) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) { + header, err := v.toHeaders(e.Context.AsV1()) + if err != nil { + return nil, err + } + + body, err := e.DataBytes() + if err != nil { + return nil, err + } + + msg := &Message{ + Header: header, + Body: body, + } + + return msg, nil +} + +func (v CodecV1) toHeaders(ec *cloudevents.EventContextV1) (http.Header, error) { + h := http.Header{} + h.Set("ce-specversion", ec.SpecVersion) + h.Set("ce-type", ec.Type) + h.Set("ce-source", ec.Source.String()) + if ec.Subject != nil { + h.Set("ce-subject", *ec.Subject) + } + h.Set("ce-id", ec.ID) + if ec.Time != nil && !ec.Time.IsZero() { + h.Set("ce-time", ec.Time.String()) + } + if ec.DataSchema != nil { + h.Set("ce-dataschema", ec.DataSchema.String()) + } + if ec.DataContentType != nil && *ec.DataContentType != "" { + h.Set("Content-Type", *ec.DataContentType) + } + + for k, v := range ec.Extensions { + k = strings.ToLower(k) + // Per spec, extensions are strings and converted to a list of headers as: + // ce-key: value + cstr, err := types.Format(v) + if err != nil { + return h, err + } + h.Set("ce-"+k, cstr) + } + + return h, nil +} + +func (v CodecV1) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { + m, ok := msg.(*Message) + if !ok { + return nil, fmt.Errorf("failed to convert transport.Message to http.Message") + } + ca, err := v.fromHeaders(m.Header) + if err != nil { + return nil, err + } + var body interface{} + if len(m.Body) > 0 { + body = m.Body + } + return &cloudevents.Event{ + Context: &ca, + Data: body, + DataEncoded: body != nil, + }, nil +} + +func (v CodecV1) fromHeaders(h http.Header) (cloudevents.EventContextV1, error) { + // Normalize headers. + for k, v := range h { + ck := textproto.CanonicalMIMEHeaderKey(k) + if k != ck { + delete(h, k) + h[ck] = v + } + } + + ec := cloudevents.EventContextV1{} + + ec.SpecVersion = h.Get("ce-specversion") + h.Del("ce-specversion") + + ec.ID = h.Get("ce-id") + h.Del("ce-id") + + ec.Type = h.Get("ce-type") + h.Del("ce-type") + + source := types.ParseURIRef(h.Get("ce-source")) + if source != nil { + ec.Source = *source + } + h.Del("ce-source") + + subject := h.Get("ce-subject") + if subject != "" { + ec.Subject = &subject + } + h.Del("ce-subject") + + var err error + ec.Time, err = types.ParseTimestamp(h.Get("ce-time")) + if err != nil { + return ec, err + } + h.Del("ce-time") + + ec.DataSchema = types.ParseURI(h.Get("ce-dataschema")) + h.Del("ce-dataschema") + + contentType := h.Get("Content-Type") + if contentType != "" { + ec.DataContentType = &contentType + } + h.Del("Content-Type") + + // At this point, we have deleted all the known headers. + // Everything left is assumed to be an extension. + + extensions := make(map[string]interface{}) + for k := range h { + k = strings.ToLower(k) + if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") { + ak := strings.ToLower(k[len("ce-"):]) + extensions[ak] = h.Get(k) + } + } + if len(extensions) > 0 { + ec.Extensions = extensions + } + return ec, nil +} + +func (v CodecV1) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { + version := msg.CloudEventsVersion() + if version != cloudevents.CloudEventsVersionV1 { + return Unknown + } + m, ok := msg.(*Message) + if !ok { + return Unknown + } + contentType := m.Header.Get("Content-Type") + if contentType == cloudevents.ApplicationCloudEventsJSON { + return StructuredV1 + } + if contentType == cloudevents.ApplicationCloudEventsBatchJSON { + return BatchedV1 + } + return BinaryV1 +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/encoding.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/encoding.go index a0d80c494..60f3e3ea3 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/encoding.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/encoding.go @@ -29,6 +29,13 @@ const ( StructuredV03 // BatchedV03 is Batched CloudEvents spec v0.3. BatchedV03 + // BinaryV1 is Binary CloudEvents spec v1.0. + BinaryV1 + // StructuredV03 is Structured CloudEvents spec v1.0. + StructuredV1 + // BatchedV1 is Batched CloudEvents spec v1.0. + BatchedV1 + // Unknown is unknown. Unknown @@ -39,6 +46,10 @@ const ( // Structured is used for Context Based Encoding Selections to use the // DefaultStructuredEncodingSelectionStrategy Structured = "structured" + + // Batched is used for Context Based Encoding Selections to use the + // DefaultStructuredEncodingSelectionStrategy + Batched = "batched" ) func ContextBasedEncodingSelectionStrategy(ctx context.Context, e cloudevents.Event) Encoding { @@ -62,6 +73,8 @@ func DefaultBinaryEncodingSelectionStrategy(ctx context.Context, e cloudevents.E return BinaryV02 case cloudevents.CloudEventsVersionV03: return BinaryV03 + case cloudevents.CloudEventsVersionV1: + return BinaryV1 } // Unknown version, return Default. return Default @@ -77,6 +90,8 @@ func DefaultStructuredEncodingSelectionStrategy(ctx context.Context, e cloudeven return StructuredV02 case cloudevents.CloudEventsVersionV03: return StructuredV03 + case cloudevents.CloudEventsVersionV1: + return StructuredV1 } // Unknown version, return Default. return Default @@ -89,23 +104,15 @@ func (e Encoding) String() string { return "Default Encoding " + e.Version() // Binary - case BinaryV01: - fallthrough - case BinaryV02: - fallthrough - case BinaryV03: + case BinaryV01, BinaryV02, BinaryV03, BinaryV1: return "Binary Encoding " + e.Version() // Structured - case StructuredV01: - fallthrough - case StructuredV02: - fallthrough - case StructuredV03: + case StructuredV01, StructuredV02, StructuredV03, StructuredV1: return "Structured Encoding " + e.Version() // Batched - case BatchedV03: + case BatchedV03, BatchedV1: return "Batched Encoding " + e.Version() default: @@ -120,25 +127,21 @@ func (e Encoding) Version() string { return "Default" // Version 0.1 - case BinaryV01: - fallthrough - case StructuredV01: + case BinaryV01, StructuredV01: return "v0.1" // Version 0.2 - case BinaryV02: - fallthrough - case StructuredV02: + case BinaryV02, StructuredV02: return "v0.2" // Version 0.3 - case BinaryV03: - fallthrough - case StructuredV03: - fallthrough - case BatchedV03: + case BinaryV03, StructuredV03, BatchedV03: return "v0.3" + // Version 1.0 + case BinaryV1, StructuredV1, BatchedV1: + return "v1.0" + // Unknown default: return "Unknown" @@ -171,8 +174,32 @@ func (e Encoding) Codec() string { case BatchedV03: return "batched/v0.3" + // Version 1.0 + case BinaryV1: + return "binary/v1.0" + case StructuredV1: + return "structured/v1.0" + case BatchedV1: + return "batched/v1.0" + // Unknown default: return "unknown" } } + +// Name creates a string to represent the the codec name. +func (e Encoding) Name() string { + switch e { + case Default: + return Binary + case BinaryV01, BinaryV02, BinaryV03, BinaryV1: + return Binary + case StructuredV01, StructuredV02, StructuredV03, StructuredV1: + return Structured + case BatchedV03, BatchedV1: + return Batched + default: + return Binary + } +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/options.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/options.go index c6e0c20df..0276157fc 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/options.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/options.go @@ -178,7 +178,7 @@ func WithPort(port int) Option { if t == nil { return fmt.Errorf("http port option can not set nil transport") } - if port < 0 { + if port < 0 || port > 65535 { return fmt.Errorf("http port option was given an invalid port: %d", port) } if err := checkListen(t, "http port option"); err != nil { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/transport.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/transport.go index 7a1548a43..4dad3d7a5 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/transport.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http/transport.go @@ -181,6 +181,7 @@ func (t *Transport) obsSend(ctx context.Context, event cloudevents.Event) (conte req.Method = t.Req.Method req.URL = t.Req.URL req.Close = t.Req.Close + req.Host = t.Req.Host copyHeadersEnsure(t.Req.Header, &req.Header) } @@ -214,14 +215,21 @@ func (t *Transport) obsSend(ctx context.Context, event cloudevents.Event) (conte }) if err != nil { isErr := true + handled := false if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok { if !txerr.IsFatal() { isErr = false } + if txerr.Handled() { + handled = true + } } if isErr { return rctx, nil, err } + if handled { + return rctx, nil, nil + } } if accepted(resp) { return rctx, respEvent, nil @@ -240,13 +248,13 @@ func (t *Transport) MessageToEvent(ctx context.Context, msg *Message) (*cloudeve if msg.CloudEventsVersion() != "" { // This is likely a cloudevents encoded message, try to decode it. if ok := t.loadCodec(ctx); !ok { - err = transport.NewErrTransportMessageConversion("http", fmt.Sprintf("unknown encoding set on transport: %d", t.Encoding), true) + err = transport.NewErrTransportMessageConversion("http", fmt.Sprintf("unknown encoding set on transport: %d", t.Encoding), false, true) logger.Error("failed to load codec", zap.Error(err)) } else { event, err = t.codec.Decode(ctx, msg) } } else { - err = transport.NewErrTransportMessageConversion("http", "cloudevents version unknown", false) + err = transport.NewErrTransportMessageConversion("http", "cloudevents version unknown", false, false) } // If codec returns and error, or could not load the correct codec, try @@ -254,12 +262,19 @@ func (t *Transport) MessageToEvent(ctx context.Context, msg *Message) (*cloudeve if err != nil && t.HasConverter() { event, err = t.Converter.Convert(ctx, msg, err) } + // If err is still set, it means that there was no converter, or the // converter failed to convert. if err != nil { logger.Debug("failed to decode message", zap.Error(err)) } + // If event and error are both nil, then there is nothing to do with this event, it was handled. + if err == nil && event == nil { + logger.Debug("convert function returned (nil, nil)") + err = transport.NewErrTransportMessageConversion("http", "convert function handled request", true, false) + } + return event, err } @@ -548,16 +563,30 @@ func (t *Transport) ServeHTTP(w http.ResponseWriter, req *http.Request) { }) if err != nil { isFatal := true + handled := false if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok { isFatal = txerr.IsFatal() + handled = txerr.Handled() } - if isFatal || event == nil { + if isFatal { logger.Errorw("failed to convert http message to event", zap.Error(err)) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(fmt.Sprintf(`{"error":%q}`, err.Error()))) r.Error() return } + // if handled, do not pass to receiver. + if handled { + w.WriteHeader(http.StatusNoContent) + r.OK() + return + } + } + if event == nil { + logger.Error("failed to get non-nil event from MessageToEvent") + w.WriteHeader(http.StatusBadRequest) + r.Error() + return } resp, err := t.invokeReceiver(ctx, *event) @@ -623,6 +652,9 @@ func (t *Transport) listen() (net.Addr, error) { port := 8080 if t.Port != nil { port = *t.Port + if port < 0 || port > 65535 { + return nil, fmt.Errorf("invalid port %d", port) + } } var err error if t.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil { diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/doc.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/doc.go index 1019b4a2d..b1d9c29da 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/doc.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/doc.go @@ -1,4 +1,41 @@ /* -Package types provides custom types to support CloudEvents. +Package types implements the CloudEvents type system. + +CloudEvents defines a set of abstract types for event context attributes. Each +type has a corresponding native Go type and a canonical string encoding. The +native Go types used to represent the CloudEvents types are: +bool, int32, string, []byte, *url.URL, time.Time + + +----------------+----------------+-----------------------------------+ + |CloudEvents Type|Native Type |Convertible From | + +================+================+===================================+ + |Bool |bool |bool | + +----------------+----------------+-----------------------------------+ + |Integer |int32 |Any numeric type with value in | + | | |range of int32 | + +----------------+----------------+-----------------------------------+ + |String |string |string | + +----------------+----------------+-----------------------------------+ + |Binary |[]byte |[]byte | + +----------------+----------------+-----------------------------------+ + |URI-Reference |*url.URL |url.URL, types.URIRef, types.URI | + +----------------+----------------+-----------------------------------+ + |URI |*url.URL |url.URL, types.URIRef, types.URI | + | | |Must be an absolute URI. | + +----------------+----------------+-----------------------------------+ + |Timestamp |time.Time |time.Time, types.Timestamp | + +----------------+----------------+-----------------------------------+ + +Extension attributes may be stored as a native type or a canonical string. The +To functions will convert to the desired from any convertible type +or from the canonical string form. + +The Parse and Format functions convert native types to/from +canonical strings. + +Note are no Parse or Format functions for URL or string. For URL use the +standard url.Parse() and url.URL.String(). The canonical string format of a +string is the string itself. + */ package types diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/timestamp.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/timestamp.go index 6534aacbb..3ae1c7def 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/timestamp.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/timestamp.go @@ -16,15 +16,12 @@ type Timestamp struct { } // ParseTimestamp attempts to parse the given time assuming RFC3339 layout -func ParseTimestamp(t string) *Timestamp { - if t == "" { - return nil +func ParseTimestamp(s string) (*Timestamp, error) { + if s == "" { + return nil, nil } - timestamp, err := time.Parse(time.RFC3339Nano, t) - if err != nil { - return nil - } - return &Timestamp{Time: timestamp} + tt, err := ParseTime(s) + return &Timestamp{Time: tt}, err } // MarshalJSON implements a custom json marshal method used when this type is @@ -33,8 +30,7 @@ func (t *Timestamp) MarshalJSON() ([]byte, error) { if t == nil || t.IsZero() { return []byte(`""`), nil } - rfc3339 := fmt.Sprintf("%q", t.UTC().Format(time.RFC3339Nano)) - return []byte(rfc3339), nil + return []byte(fmt.Sprintf("%q", t)), nil } // UnmarshalJSON implements the json unmarshal method used when this type is @@ -44,10 +40,9 @@ func (t *Timestamp) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, ×tamp); err != nil { return err } - if pt := ParseTimestamp(timestamp); pt != nil { - *t = *pt - } - return nil + var err error + t.Time, err = ParseTime(timestamp) + return err } // MarshalXML implements a custom xml marshal method used when this type is @@ -56,8 +51,7 @@ func (t *Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if t == nil || t.IsZero() { return e.EncodeElement(nil, start) } - v := t.UTC().Format(time.RFC3339Nano) - return e.EncodeElement(v, start) + return e.EncodeElement(t.String(), start) } // UnmarshalXML implements the xml unmarshal method used when this type is @@ -67,17 +61,10 @@ func (t *Timestamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if err := d.DecodeElement(×tamp, &start); err != nil { return err } - if pt := ParseTimestamp(timestamp); pt != nil { - *t = *pt - } - return nil + var err error + t.Time, err = ParseTime(timestamp) + return err } -// String outputs the time using layout RFC3339. -func (t *Timestamp) String() string { - if t == nil { - return time.Time{}.UTC().Format(time.RFC3339Nano) - } - - return t.UTC().Format(time.RFC3339Nano) -} +// String outputs the time using RFC3339 format. +func (t Timestamp) String() string { return FormatTime(t.Time) } diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uri.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uri.go new file mode 100644 index 000000000..97248a24d --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uri.go @@ -0,0 +1,77 @@ +package types + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "net/url" +) + +// URI is a wrapper to url.URL. It is intended to enforce compliance with +// the CloudEvents spec for their definition of URI. Custom +// marshal methods are implemented to ensure the outbound URI object +// is a flat string. +type URI struct { + url.URL +} + +// ParseURI attempts to parse the given string as a URI. +func ParseURI(u string) *URI { + if u == "" { + return nil + } + pu, err := url.Parse(u) + if err != nil { + return nil + } + return &URI{URL: *pu} +} + +// MarshalJSON implements a custom json marshal method used when this type is +// marshaled using json.Marshal. +func (u URI) MarshalJSON() ([]byte, error) { + b := fmt.Sprintf("%q", u.String()) + return []byte(b), nil +} + +// UnmarshalJSON implements the json unmarshal method used when this type is +// unmarshaled using json.Unmarshal. +func (u *URI) UnmarshalJSON(b []byte) error { + var ref string + if err := json.Unmarshal(b, &ref); err != nil { + return err + } + r := ParseURI(ref) + if r != nil { + *u = *r + } + return nil +} + +// MarshalXML implements a custom xml marshal method used when this type is +// marshaled using xml.Marshal. +func (u URI) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(u.String(), start) +} + +// UnmarshalXML implements the xml unmarshal method used when this type is +// unmarshaled using xml.Unmarshal. +func (u *URI) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var ref string + if err := d.DecodeElement(&ref, &start); err != nil { + return err + } + r := ParseURI(ref) + if r != nil { + *u = *r + } + return nil +} + +// String returns the full string representation of the URI-Reference. +func (u *URI) String() string { + if u == nil { + return "" + } + return u.URL.String() +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uriref.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uriref.go new file mode 100644 index 000000000..e19a1dbb7 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/uriref.go @@ -0,0 +1,77 @@ +package types + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "net/url" +) + +// URIRef is a wrapper to url.URL. It is intended to enforce compliance with +// the CloudEvents spec for their definition of URI-Reference. Custom +// marshal methods are implemented to ensure the outbound URIRef object is +// is a flat string. +type URIRef struct { + url.URL +} + +// ParseURIRef attempts to parse the given string as a URI-Reference. +func ParseURIRef(u string) *URIRef { + if u == "" { + return nil + } + pu, err := url.Parse(u) + if err != nil { + return nil + } + return &URIRef{URL: *pu} +} + +// MarshalJSON implements a custom json marshal method used when this type is +// marshaled using json.Marshal. +func (u URIRef) MarshalJSON() ([]byte, error) { + b := fmt.Sprintf("%q", u.String()) + return []byte(b), nil +} + +// UnmarshalJSON implements the json unmarshal method used when this type is +// unmarshaled using json.Unmarshal. +func (u *URIRef) UnmarshalJSON(b []byte) error { + var ref string + if err := json.Unmarshal(b, &ref); err != nil { + return err + } + r := ParseURIRef(ref) + if r != nil { + *u = *r + } + return nil +} + +// MarshalXML implements a custom xml marshal method used when this type is +// marshaled using xml.Marshal. +func (u URIRef) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(u.String(), start) +} + +// UnmarshalXML implements the xml unmarshal method used when this type is +// unmarshaled using xml.Unmarshal. +func (u *URIRef) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var ref string + if err := d.DecodeElement(&ref, &start); err != nil { + return err + } + r := ParseURIRef(ref) + if r != nil { + *u = *r + } + return nil +} + +// String returns the full string representation of the URI-Reference. +func (u *URIRef) String() string { + if u == nil { + return "" + } + return u.URL.String() +} diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/urlref.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/urlref.go index 2743c45e2..2578801cd 100644 --- a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/urlref.go +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/urlref.go @@ -11,6 +11,8 @@ import ( // the CloudEvents spec for their definition of URI-Reference. Custom // marshal methods are implemented to ensure the outbound URLRef object is // is a flat string. +// +// deprecated: use URIRef. type URLRef struct { url.URL } diff --git a/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/value.go b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/value.go new file mode 100644 index 000000000..803be2f11 --- /dev/null +++ b/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents/types/value.go @@ -0,0 +1,260 @@ +package types + +import ( + "encoding/base64" + "fmt" + "math" + "net/url" + "reflect" + "strconv" + "time" +) + +// FormatBool returns canonical string format: "true" or "false" +func FormatBool(v bool) string { return strconv.FormatBool(v) } + +// FormatInteger returns canonical string format: decimal notation. +func FormatInteger(v int32) string { return strconv.Itoa(int(v)) } + +// FormatBinary returns canonical string format: standard base64 encoding +func FormatBinary(v []byte) string { return base64.StdEncoding.EncodeToString(v) } + +// FormatTime returns canonical string format: RFC3339 with nanoseconds +func FormatTime(v time.Time) string { return v.UTC().Format(time.RFC3339Nano) } + +// ParseBool parse canonical string format: "true" or "false" +func ParseBool(v string) (bool, error) { return strconv.ParseBool(v) } + +// ParseInteger parse canonical string format: decimal notation. +func ParseInteger(v string) (int32, error) { + // Accept floating-point but truncate to int32 as per CE spec. + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, err + } + if f > math.MaxInt32 || f < math.MinInt32 { + return 0, rangeErr(v) + } + return int32(f), nil +} + +// ParseBinary parse canonical string format: standard base64 encoding +func ParseBinary(v string) ([]byte, error) { return base64.StdEncoding.DecodeString(v) } + +// ParseTime parse canonical string format: RFC3339 with nanoseconds +func ParseTime(v string) (time.Time, error) { + t, err := time.Parse(time.RFC3339Nano, v) + if err != nil { + err := convertErr(time.Time{}, v) + err.extra = ": not in RFC3339 format" + return time.Time{}, err + } + return t, nil +} + +// Format returns the canonical string format of v, where v can be +// any type that is convertible to a CloudEvents type. +func Format(v interface{}) (string, error) { + v, err := Validate(v) + if err != nil { + return "", err + } + switch v := v.(type) { + case bool: + return FormatBool(v), nil + case int32: + return FormatInteger(v), nil + case string: + return v, nil + case []byte: + return FormatBinary(v), nil + case url.URL: + return v.String(), nil + case *url.URL: + // url.URL is often passed by pointer so allow both + return v.String(), nil + case time.Time: + return FormatTime(v), nil + default: + return "", fmt.Errorf("%T is not a CloudEvents type", v) + } +} + +// Validate v is a valid CloudEvents attribute value, convert it to one of: +// bool, int32, string, []byte, *url.URL, time.Time +func Validate(v interface{}) (interface{}, error) { + switch v := v.(type) { + case bool, int32, string, []byte, time.Time: + return v, nil // Already a CloudEvents type, no validation needed. + + case uint, uintptr, uint8, uint16, uint32, uint64: + u := reflect.ValueOf(v).Uint() + if u > math.MaxInt32 { + return nil, rangeErr(v) + } + return int32(u), nil + case int, int8, int16, int64: + i := reflect.ValueOf(v).Int() + if i > math.MaxInt32 || i < math.MinInt32 { + return nil, rangeErr(v) + } + return int32(i), nil + case float32, float64: + f := reflect.ValueOf(v).Float() + if f > math.MaxInt32 || f < math.MinInt32 { + return nil, rangeErr(v) + } + return int32(f), nil + + case *url.URL: + if v == nil { + break + } + return v, nil + case url.URL: + return &v, nil + case URIRef: + return &v.URL, nil + case URI: + return &v.URL, nil + case URLRef: + return &v.URL, nil + + case Timestamp: + return v.Time, nil + } + rx := reflect.ValueOf(v) + if rx.Kind() == reflect.Ptr && !rx.IsNil() { + // Allow pointers-to convertible types + return Validate(rx.Elem().Interface()) + } + return nil, fmt.Errorf("invalid CloudEvents value: %#v", v) +} + +// ToBool accepts a bool value or canonical "true"/"false" string. +func ToBool(v interface{}) (bool, error) { + v, err := Validate(v) + if err != nil { + return false, err + } + switch v := v.(type) { + case bool: + return v, nil + case string: + return ParseBool(v) + default: + return false, convertErr(true, v) + } +} + +// ToInteger accepts any numeric value in int32 range, or canonical string. +func ToInteger(v interface{}) (int32, error) { + v, err := Validate(v) + if err != nil { + return 0, err + } + switch v := v.(type) { + case int32: + return v, nil + case string: + return ParseInteger(v) + default: + return 0, convertErr(int32(0), v) + } +} + +// ToString returns a string value unaltered. +// +// This function does not perform canonical string encoding, use one of the +// Format functions for that. +func ToString(v interface{}) (string, error) { + v, err := Validate(v) + if err != nil { + return "", err + } + switch v := v.(type) { + case string: + return v, nil + default: + return "", convertErr("", v) + } +} + +// ToBinary returns a []byte value, decoding from base64 string if necessary. +func ToBinary(v interface{}) ([]byte, error) { + v, err := Validate(v) + if err != nil { + return nil, err + } + switch v := v.(type) { + case []byte: + return v, nil + case string: + return base64.StdEncoding.DecodeString(v) + default: + return nil, convertErr([]byte(nil), v) + } +} + +// ToURL returns a *url.URL value, parsing from string if necessary. +func ToURL(v interface{}) (*url.URL, error) { + v, err := Validate(v) + if err != nil { + return nil, err + } + switch v := v.(type) { + case *url.URL: + return v, nil + case string: + u, err := url.Parse(v) + if err != nil { + return nil, err + } + return u, nil + default: + return nil, convertErr((*url.URL)(nil), v) + } +} + +// ToTime returns a time.Time value, parsing from RFC3339 string if necessary. +func ToTime(v interface{}) (time.Time, error) { + v, err := Validate(v) + if err != nil { + return time.Time{}, err + } + switch v := v.(type) { + case time.Time: + return v, nil + case string: + ts, err := time.Parse(time.RFC3339Nano, v) + if err != nil { + return time.Time{}, err + } + return ts, nil + default: + return time.Time{}, convertErr(time.Time{}, v) + } +} + +type ConvertErr struct { + // Value being converted + Value interface{} + // Type of attempted conversion + Type reflect.Type + + extra string +} + +func (e *ConvertErr) Error() string { + return fmt.Sprintf("cannot convert %#v to %s%s", e.Value, e.Type, e.extra) +} + +func convertErr(target, v interface{}) *ConvertErr { + return &ConvertErr{Value: v, Type: reflect.TypeOf(target)} +} + +func rangeErr(v interface{}) error { + e := convertErr(int32(0), v) + e.extra = ": out of range" + return e +} diff --git a/vendor/github.com/kelseyhightower/envconfig/LICENSE b/vendor/github.com/kelseyhightower/envconfig/LICENSE new file mode 100644 index 000000000..4bfa7a84d --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Kelsey Hightower + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kelseyhightower/envconfig/doc.go b/vendor/github.com/kelseyhightower/envconfig/doc.go new file mode 100644 index 000000000..f28561cd1 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +// Package envconfig implements decoding of environment variables based on a user +// defined specification. A typical use is using environment variables for +// configuration settings. +package envconfig diff --git a/vendor/github.com/kelseyhightower/envconfig/env_os.go b/vendor/github.com/kelseyhightower/envconfig/env_os.go new file mode 100644 index 000000000..eba07a6c6 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/env_os.go @@ -0,0 +1,7 @@ +// +build appengine go1.5 + +package envconfig + +import "os" + +var lookupEnv = os.LookupEnv diff --git a/vendor/github.com/kelseyhightower/envconfig/env_syscall.go b/vendor/github.com/kelseyhightower/envconfig/env_syscall.go new file mode 100644 index 000000000..425454008 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/env_syscall.go @@ -0,0 +1,7 @@ +// +build !appengine,!go1.5 + +package envconfig + +import "syscall" + +var lookupEnv = syscall.Getenv diff --git a/vendor/github.com/kelseyhightower/envconfig/envconfig.go b/vendor/github.com/kelseyhightower/envconfig/envconfig.go new file mode 100644 index 000000000..3f16108db --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/envconfig.go @@ -0,0 +1,382 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "encoding" + "errors" + "fmt" + "os" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +// ErrInvalidSpecification indicates that a specification is of the wrong type. +var ErrInvalidSpecification = errors.New("specification must be a struct pointer") + +var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)") +var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)") + +// A ParseError occurs when an environment variable cannot be converted to +// the type required by a struct field during assignment. +type ParseError struct { + KeyName string + FieldName string + TypeName string + Value string + Err error +} + +// Decoder has the same semantics as Setter, but takes higher precedence. +// It is provided for historical compatibility. +type Decoder interface { + Decode(value string) error +} + +// Setter is implemented by types can self-deserialize values. +// Any type that implements flag.Value also implements Setter. +type Setter interface { + Set(value string) error +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err) +} + +// varInfo maintains information about the configuration variable +type varInfo struct { + Name string + Alt string + Key string + Field reflect.Value + Tags reflect.StructTag +} + +// GatherInfo gathers information about the specified struct +func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) { + s := reflect.ValueOf(spec) + + if s.Kind() != reflect.Ptr { + return nil, ErrInvalidSpecification + } + s = s.Elem() + if s.Kind() != reflect.Struct { + return nil, ErrInvalidSpecification + } + typeOfSpec := s.Type() + + // over allocate an info array, we will extend if needed later + infos := make([]varInfo, 0, s.NumField()) + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + ftype := typeOfSpec.Field(i) + if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) { + continue + } + + for f.Kind() == reflect.Ptr { + if f.IsNil() { + if f.Type().Elem().Kind() != reflect.Struct { + // nil pointer to a non-struct: leave it alone + break + } + // nil pointer to struct: create a zero instance + f.Set(reflect.New(f.Type().Elem())) + } + f = f.Elem() + } + + // Capture information about the config variable + info := varInfo{ + Name: ftype.Name, + Field: f, + Tags: ftype.Tag, + Alt: strings.ToUpper(ftype.Tag.Get("envconfig")), + } + + // Default to the field name as the env var name (will be upcased) + info.Key = info.Name + + // Best effort to un-pick camel casing as separate words + if isTrue(ftype.Tag.Get("split_words")) { + words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1) + if len(words) > 0 { + var name []string + for _, words := range words { + if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 { + name = append(name, m[1], m[2]) + } else { + name = append(name, words[0]) + } + } + + info.Key = strings.Join(name, "_") + } + } + if info.Alt != "" { + info.Key = info.Alt + } + if prefix != "" { + info.Key = fmt.Sprintf("%s_%s", prefix, info.Key) + } + info.Key = strings.ToUpper(info.Key) + infos = append(infos, info) + + if f.Kind() == reflect.Struct { + // honor Decode if present + if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil { + innerPrefix := prefix + if !ftype.Anonymous { + innerPrefix = info.Key + } + + embeddedPtr := f.Addr().Interface() + embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr) + if err != nil { + return nil, err + } + infos = append(infos[:len(infos)-1], embeddedInfos...) + + continue + } + } + } + return infos, nil +} + +// CheckDisallowed checks that no environment variables with the prefix are set +// that we don't know how or want to parse. This is likely only meaningful with +// a non-empty prefix. +func CheckDisallowed(prefix string, spec interface{}) error { + infos, err := gatherInfo(prefix, spec) + if err != nil { + return err + } + + vars := make(map[string]struct{}) + for _, info := range infos { + vars[info.Key] = struct{}{} + } + + if prefix != "" { + prefix = strings.ToUpper(prefix) + "_" + } + + for _, env := range os.Environ() { + if !strings.HasPrefix(env, prefix) { + continue + } + v := strings.SplitN(env, "=", 2)[0] + if _, found := vars[v]; !found { + return fmt.Errorf("unknown environment variable %s", v) + } + } + + return nil +} + +// Process populates the specified struct based on environment variables +func Process(prefix string, spec interface{}) error { + infos, err := gatherInfo(prefix, spec) + + for _, info := range infos { + + // `os.Getenv` cannot differentiate between an explicitly set empty value + // and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`, + // but it is only available in go1.5 or newer. We're using Go build tags + // here to use os.LookupEnv for >=go1.5 + value, ok := lookupEnv(info.Key) + if !ok && info.Alt != "" { + value, ok = lookupEnv(info.Alt) + } + + def := info.Tags.Get("default") + if def != "" && !ok { + value = def + } + + req := info.Tags.Get("required") + if !ok && def == "" { + if isTrue(req) { + key := info.Key + if info.Alt != "" { + key = info.Alt + } + return fmt.Errorf("required key %s missing value", key) + } + continue + } + + err = processField(value, info.Field) + if err != nil { + return &ParseError{ + KeyName: info.Key, + FieldName: info.Name, + TypeName: info.Field.Type().String(), + Value: value, + Err: err, + } + } + } + + return err +} + +// MustProcess is the same as Process but panics if an error occurs +func MustProcess(prefix string, spec interface{}) { + if err := Process(prefix, spec); err != nil { + panic(err) + } +} + +func processField(value string, field reflect.Value) error { + typ := field.Type() + + decoder := decoderFrom(field) + if decoder != nil { + return decoder.Decode(value) + } + // look for Set method if Decode not defined + setter := setterFrom(field) + if setter != nil { + return setter.Set(value) + } + + if t := textUnmarshaler(field); t != nil { + return t.UnmarshalText([]byte(value)) + } + + if b := binaryUnmarshaler(field); b != nil { + return b.UnmarshalBinary([]byte(value)) + } + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + if field.IsNil() { + field.Set(reflect.New(typ)) + } + field = field.Elem() + } + + switch typ.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var ( + val int64 + err error + ) + if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" { + var d time.Duration + d, err = time.ParseDuration(value) + val = int64(d) + } else { + val, err = strconv.ParseInt(value, 0, typ.Bits()) + } + if err != nil { + return err + } + + field.SetInt(val) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := strconv.ParseUint(value, 0, typ.Bits()) + if err != nil { + return err + } + field.SetUint(val) + case reflect.Bool: + val, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.SetBool(val) + case reflect.Float32, reflect.Float64: + val, err := strconv.ParseFloat(value, typ.Bits()) + if err != nil { + return err + } + field.SetFloat(val) + case reflect.Slice: + sl := reflect.MakeSlice(typ, 0, 0) + if typ.Elem().Kind() == reflect.Uint8 { + sl = reflect.ValueOf([]byte(value)) + } else if len(strings.TrimSpace(value)) != 0 { + vals := strings.Split(value, ",") + sl = reflect.MakeSlice(typ, len(vals), len(vals)) + for i, val := range vals { + err := processField(val, sl.Index(i)) + if err != nil { + return err + } + } + } + field.Set(sl) + case reflect.Map: + mp := reflect.MakeMap(typ) + if len(strings.TrimSpace(value)) != 0 { + pairs := strings.Split(value, ",") + for _, pair := range pairs { + kvpair := strings.Split(pair, ":") + if len(kvpair) != 2 { + return fmt.Errorf("invalid map item: %q", pair) + } + k := reflect.New(typ.Key()).Elem() + err := processField(kvpair[0], k) + if err != nil { + return err + } + v := reflect.New(typ.Elem()).Elem() + err = processField(kvpair[1], v) + if err != nil { + return err + } + mp.SetMapIndex(k, v) + } + } + field.Set(mp) + } + + return nil +} + +func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) { + // it may be impossible for a struct field to fail this check + if !field.CanInterface() { + return + } + var ok bool + fn(field.Interface(), &ok) + if !ok && field.CanAddr() { + fn(field.Addr().Interface(), &ok) + } +} + +func decoderFrom(field reflect.Value) (d Decoder) { + interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) }) + return d +} + +func setterFrom(field reflect.Value) (s Setter) { + interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) }) + return s +} + +func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) { + interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) }) + return t +} + +func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) { + interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) }) + return b +} + +func isTrue(s string) bool { + b, _ := strconv.ParseBool(s) + return b +} diff --git a/vendor/github.com/kelseyhightower/envconfig/usage.go b/vendor/github.com/kelseyhightower/envconfig/usage.go new file mode 100644 index 000000000..1e6d0a8f3 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/usage.go @@ -0,0 +1,164 @@ +// Copyright (c) 2016 Kelsey Hightower and others. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "encoding" + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" + "text/tabwriter" + "text/template" +) + +const ( + // DefaultListFormat constant to use to display usage in a list format + DefaultListFormat = `This application is configured via the environment. The following environment +variables can be used: +{{range .}} +{{usage_key .}} + [description] {{usage_description .}} + [type] {{usage_type .}} + [default] {{usage_default .}} + [required] {{usage_required .}}{{end}} +` + // DefaultTableFormat constant to use to display usage in a tabular format + DefaultTableFormat = `This application is configured via the environment. The following environment +variables can be used: + +KEY TYPE DEFAULT REQUIRED DESCRIPTION +{{range .}}{{usage_key .}} {{usage_type .}} {{usage_default .}} {{usage_required .}} {{usage_description .}} +{{end}}` +) + +var ( + decoderType = reflect.TypeOf((*Decoder)(nil)).Elem() + setterType = reflect.TypeOf((*Setter)(nil)).Elem() + textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem() +) + +func implementsInterface(t reflect.Type) bool { + return t.Implements(decoderType) || + reflect.PtrTo(t).Implements(decoderType) || + t.Implements(setterType) || + reflect.PtrTo(t).Implements(setterType) || + t.Implements(textUnmarshalerType) || + reflect.PtrTo(t).Implements(textUnmarshalerType) || + t.Implements(binaryUnmarshalerType) || + reflect.PtrTo(t).Implements(binaryUnmarshalerType) +} + +// toTypeDescription converts Go types into a human readable description +func toTypeDescription(t reflect.Type) string { + switch t.Kind() { + case reflect.Array, reflect.Slice: + if t.Elem().Kind() == reflect.Uint8 { + return "String" + } + return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem())) + case reflect.Map: + return fmt.Sprintf( + "Comma-separated list of %s:%s pairs", + toTypeDescription(t.Key()), + toTypeDescription(t.Elem()), + ) + case reflect.Ptr: + return toTypeDescription(t.Elem()) + case reflect.Struct: + if implementsInterface(t) && t.Name() != "" { + return t.Name() + } + return "" + case reflect.String: + name := t.Name() + if name != "" && name != "string" { + return name + } + return "String" + case reflect.Bool: + name := t.Name() + if name != "" && name != "bool" { + return name + } + return "True or False" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "int") { + return name + } + return "Integer" + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "uint") { + return name + } + return "Unsigned Integer" + case reflect.Float32, reflect.Float64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "float") { + return name + } + return "Float" + } + return fmt.Sprintf("%+v", t) +} + +// Usage writes usage information to stdout using the default header and table format +func Usage(prefix string, spec interface{}) error { + // The default is to output the usage information as a table + // Create tabwriter instance to support table output + tabs := tabwriter.NewWriter(os.Stdout, 1, 0, 4, ' ', 0) + + err := Usagef(prefix, spec, tabs, DefaultTableFormat) + tabs.Flush() + return err +} + +// Usagef writes usage information to the specified io.Writer using the specifed template specification +func Usagef(prefix string, spec interface{}, out io.Writer, format string) error { + + // Specify the default usage template functions + functions := template.FuncMap{ + "usage_key": func(v varInfo) string { return v.Key }, + "usage_description": func(v varInfo) string { return v.Tags.Get("desc") }, + "usage_type": func(v varInfo) string { return toTypeDescription(v.Field.Type()) }, + "usage_default": func(v varInfo) string { return v.Tags.Get("default") }, + "usage_required": func(v varInfo) (string, error) { + req := v.Tags.Get("required") + if req != "" { + reqB, err := strconv.ParseBool(req) + if err != nil { + return "", err + } + if reqB { + req = "true" + } + } + return req, nil + }, + } + + tmpl, err := template.New("envconfig").Funcs(functions).Parse(format) + if err != nil { + return err + } + + return Usaget(prefix, spec, out, tmpl) +} + +// Usaget writes usage information to the specified io.Writer using the specified template +func Usaget(prefix string, spec interface{}, out io.Writer, tmpl *template.Template) error { + // gather first + infos, err := gatherInfo(prefix, spec) + if err != nil { + return err + } + + return tmpl.Execute(out, infos) +}