From a8a4a812b49eb212aed971e109d8e62e222542a0 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Fri, 5 Apr 2024 14:15:06 -0500 Subject: [PATCH] webhook: use new json package Signed-off-by: Hank Donnay --- notifier/webhook/benchmarks_test.go | 72 +++++++++++++++++++++++++++++ notifier/webhook/deliverer.go | 55 ++++++++++++++++++---- notifier/webhook/deliverer_test.go | 47 +++++++++++++++++++ 3 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 notifier/webhook/benchmarks_test.go diff --git a/notifier/webhook/benchmarks_test.go b/notifier/webhook/benchmarks_test.go new file mode 100644 index 0000000000..8ab2395afa --- /dev/null +++ b/notifier/webhook/benchmarks_test.go @@ -0,0 +1,72 @@ +package webhook + +import ( + stdjson "encoding/json" + "io" + "net/url" + "testing" + + "github.com/google/uuid" + + "github.com/quay/clair/v4/internal/codec" + "github.com/quay/clair/v4/internal/json" + "github.com/quay/clair/v4/notifier" +) + +func BenchmarkEncodingJSON(b *testing.B) { + enc := stdjson.NewEncoder(io.Discard) + obj := notifier.Callback{ + NotificationID: uuid.New(), + } + if err := obj.Callback.UnmarshalBinary([]byte("http://example.com/")); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := enc.Encode(&obj) + if err != nil { + b.Error(err) + } + } +} + +func BenchmarkCodecJSON(b *testing.B) { + enc := codec.GetEncoder(io.Discard) + obj := notifier.Callback{ + NotificationID: uuid.New(), + } + if err := obj.Callback.UnmarshalBinary([]byte("http://example.com/")); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := enc.Encode(&obj) + if err != nil { + b.Error(err) + } + } +} + +func BenchmarkExperimentalJSON(b *testing.B) { + id := uuid.New() + url, err := url.Parse("http://example.com") + if err != nil { + b.Fatal(err) + } + obj := callbackRequest{ + ID: &id, + URL: url, + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := json.MarshalWrite(io.Discard, &obj, options()) + if err != nil { + b.Error(err) + } + } +} diff --git a/notifier/webhook/deliverer.go b/notifier/webhook/deliverer.go index ff756b9db4..3bfb41acb5 100644 --- a/notifier/webhook/deliverer.go +++ b/notifier/webhook/deliverer.go @@ -3,6 +3,7 @@ package webhook import ( "context" "errors" + "io" "net/http" "net/url" "sync" @@ -12,9 +13,9 @@ import ( "github.com/quay/zlog" clairerror "github.com/quay/clair/v4/clair-error" - "github.com/quay/clair/v4/internal/codec" "github.com/quay/clair/v4/internal/httputil" - "github.com/quay/clair/v4/notifier" + "github.com/quay/clair/v4/internal/json" + "github.com/quay/clair/v4/internal/json/jsontext" ) // SignedOnce is used to print a deprecation notice, but only once per run. @@ -67,26 +68,60 @@ func (d *Deliverer) Name() string { return "webhook" } +var options = sync.OnceValue(func() json.Options { + return json.WithMarshalers(json.MarshalFuncV2(marshalCallback)) +}) + +func marshalCallback(enc *jsontext.Encoder, cb *callbackRequest, opts json.Options) error { + if err := enc.WriteToken(jsontext.ObjectStart); err != nil { + return err + } + if err := enc.WriteToken(jsontext.String(`callback`)); err != nil { + return err + } + if err := enc.WriteToken(jsontext.String(cb.URL.String())); err != nil { + return err + } + if err := enc.WriteToken(jsontext.String(`notification_id`)); err != nil { + return err + } + if err := enc.WriteToken(jsontext.String(cb.ID.String())); err != nil { + return err + } + return enc.WriteToken(jsontext.ObjectEnd) +} + +type callbackRequest struct { + ID *uuid.UUID + URL *url.URL +} + // Deliver implements the notifier.Deliverer interface. // -// Deliver POSTS a webhook data structure to the configured target. +// Deliver POSTs a webhook data structure to the configured target. func (d *Deliverer) Deliver(ctx context.Context, nID uuid.UUID) error { ctx = zlog.ContextWithValues(ctx, "component", "notifier/webhook/Deliverer.Deliver", "notification_id", nID.String(), ) - callback, err := d.callback.Parse(nID.String()) + url, err := d.callback.Parse(nID.String()) if err != nil { return err } - - wh := notifier.Callback{ - NotificationID: nID, - Callback: *callback, + cb := callbackRequest{ + ID: &nID, + URL: url, } - req, err := httputil.NewRequestWithContext(ctx, http.MethodPost, d.target.String(), codec.JSONReader(&wh)) + rd, wr := io.Pipe() + defer rd.Close() + go func() { + err := json.MarshalWrite(wr, &cb, options()) + wr.CloseWithError(err) + }() + + req, err := httputil.NewRequestWithContext(ctx, http.MethodPost, d.target.String(), rd) if err != nil { return err } @@ -102,7 +137,7 @@ func (d *Deliverer) Deliver(ctx context.Context, nID uuid.UUID) error { } zlog.Info(ctx). - Stringer("callback", callback). + Stringer("callback", url). Stringer("target", d.target). Msg("dispatching webhook") diff --git a/notifier/webhook/deliverer_test.go b/notifier/webhook/deliverer_test.go index 8aa67c7f4f..861c10d926 100644 --- a/notifier/webhook/deliverer_test.go +++ b/notifier/webhook/deliverer_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "net/url" "path" + "strings" "sync" "testing" @@ -15,6 +16,7 @@ import ( "github.com/quay/clair/config" "github.com/quay/zlog" + json2 "github.com/quay/clair/v4/internal/json" "github.com/quay/clair/v4/notifier" ) @@ -81,3 +83,48 @@ func TestDeliverer(t *testing.T) { t.Fatalf("got: %v, wanted: %v", got, want) } } + +func TestMarshal(t *testing.T) { + // Check that the "v0" format (no specific media type) is identical to + // stdlib output. + t.Run("V0", func(t *testing.T) { + var ( + callback = "http://clair-notifier/notifier/api/v1/notification/" + noteID = uuid.New() + ) + + want := func() string { + v := notifier.Callback{ + NotificationID: noteID, + } + if err := v.Callback.UnmarshalBinary([]byte(callback)); err != nil { + t.Error(err) + } + b, err := json.Marshal(&v) + if err != nil { + t.Error(err) + } + return string(b) + }() + + got := func() string { + url, err := url.Parse(callback) + if err != nil { + t.Error(err) + } + var b strings.Builder + cb := callbackRequest{ + ID: ¬eID, + URL: url, + } + if err := json2.MarshalWrite(&b, &cb, options()); err != nil { + t.Error(err) + } + return b.String() + }() + + if !cmp.Equal(got, want) { + t.Error(cmp.Diff(got, want)) + } + }) +}