diff --git a/httptransport/common.go b/httptransport/common.go index 76bab42cbb..4931194f60 100644 --- a/httptransport/common.go +++ b/httptransport/common.go @@ -13,11 +13,6 @@ import ( "github.com/quay/claircore" ) -const ( - metricNamespace = `clair` - metricSubsystem = `http` -) - // GetDigest removes the last path element and parses it as a digest. func getDigest(_ http.ResponseWriter, r *http.Request) (d claircore.Digest, err error) { dStr := path.Base(r.URL.Path) diff --git a/httptransport/indexer_v1.go b/httptransport/indexer_v1.go index badc5291f8..c89de068ae 100644 --- a/httptransport/indexer_v1.go +++ b/httptransport/indexer_v1.go @@ -294,7 +294,7 @@ func (h *IndexerV1) affectedManifests(w http.ResponseWriter, r *http.Request) { } func init() { - indexerv1wrapper.init() + indexerv1wrapper.init("indexerv1") } var indexerv1wrapper = &wrapper{ diff --git a/httptransport/instrumentation.go b/httptransport/instrumentation.go index e66aa5938c..fcfd7ce3ed 100644 --- a/httptransport/instrumentation.go +++ b/httptransport/instrumentation.go @@ -8,6 +8,11 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) +const ( + metricNamespace = `clair` + metricSubsystem = `http` +) + type wrapper struct { RequestCount *prometheus.CounterVec RequestSize *prometheus.HistogramVec @@ -16,7 +21,62 @@ type wrapper struct { InFlight *prometheus.GaugeVec } -func (m *wrapper) init() { +func (m *wrapper) init(name string) { + if m.RequestCount == nil { + m.RequestCount = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metricNamespace, + Subsystem: metricSubsystem, + Name: name + "_request_total", + Help: "A total count of http requests for the given path", + }, + []string{"handler", "code", "method"}, + ) + } + if m.RequestSize == nil { + m.RequestSize = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricNamespace, + Subsystem: metricSubsystem, + Name: name + "_request_size_bytes", + Help: "Distribution of request sizes for the given path", + }, + []string{"handler", "code", "method"}, + ) + } + if m.ResponseSize == nil { + m.ResponseSize = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricNamespace, + Subsystem: metricSubsystem, + Name: name + "_response_size_bytes", + Help: "Distribution of response sizes for the given path", + }, []string{"handler", "code", "method"}, + ) + } + if m.RequestDuration == nil { + m.RequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricNamespace, + Subsystem: metricSubsystem, + Name: name + "_request_duration_seconds", + Help: "Distribution of request durations for the given path", + // These are roughly exponential from 0.5 to 300 seconds + Buckets: []float64{0.5, 0.7, 1.1, 1.7, 2.7, 4.2, 6.5, 10, 15, 23, 36, 54, 83, 128, 196, 300}, + }, []string{"handler", "code", "method"}, + ) + } + if m.InFlight == nil { + m.InFlight = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Subsystem: metricSubsystem, + Name: name + "_in_flight_requests", + Help: "Gauge of requests in flight", + }, + []string{"handler"}, + ) + } prometheus.MustRegister(m.RequestCount, m.RequestSize, m.ResponseSize, m.RequestDuration, m.InFlight) } diff --git a/httptransport/matcher_v1.go b/httptransport/matcher_v1.go index dd04dbbf01..27317d9dcc 100644 --- a/httptransport/matcher_v1.go +++ b/httptransport/matcher_v1.go @@ -262,7 +262,7 @@ func (h *MatcherV1) updateOperationHandlerDelete(w http.ResponseWriter, r *http. } func init() { - matcherv1wrapper.init() + matcherv1wrapper.init("matcherv1") } var matcherv1wrapper = &wrapper{ diff --git a/httptransport/notification_v1.go b/httptransport/notification_v1.go new file mode 100644 index 0000000000..968aaab34e --- /dev/null +++ b/httptransport/notification_v1.go @@ -0,0 +1,178 @@ +package httptransport + +import ( + "context" + "errors" + "net/http" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/google/uuid" + "github.com/ldelossa/responserecorder" + "github.com/quay/zlog" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/quay/clair/v4/internal/codec" + "github.com/quay/clair/v4/notifier" +) + +const defaultPageSize = 500 + +type notificationResponse struct { + Page notifier.Page `json:"page"` + Notifications []notifier.Notification `json:"notifications"` +} + +// NotificationV1 is a Notification endpoint. +type NotificationV1 struct { + inner http.Handler + serv notifier.Service +} + +var _ http.Handler = (*NotificationV1)(nil) + +// NewNotificationV1 returns an http.Handler serving the Notification V1 API rooted at +// "prefix". +func NewNotificationV1(_ context.Context, prefix string, srv notifier.Service, topt otelhttp.Option) (*NotificationV1, error) { + prefix = path.Join("/", prefix) // Ensure the prefix is rooted and cleaned. + m := http.NewServeMux() + h := NotificationV1{ + inner: otelhttp.NewHandler( + m, + "notificationv1", + otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents), + topt, + ), + serv: srv, + } + p := path.Join(prefix, "notification") + "/" + m.Handle(p, notificationv1wrapper.wrapFunc(path.Join(p, ":id"), h.serveHTTP)) + return &h, nil +} + +// ServeHTTP implements http.Handler. +func (h *NotificationV1) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() + wr := responserecorder.NewResponseRecorder(w) + defer func() { + if f, ok := wr.(http.Flusher); ok { + f.Flush() + } + zlog.Info(r.Context()). + Str("remote_addr", r.RemoteAddr). + Str("method", r.Method). + Str("request_uri", r.RequestURI). + Int("status", wr.StatusCode()). + Dur("duration", time.Since(start)). + Msg("handled HTTP request") + }() + h.inner.ServeHTTP(wr, r) +} + +func (h *NotificationV1) serveHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.get(w, r) + case http.MethodDelete: + h.delete(w, r) + default: + apiError(w, http.StatusMethodNotAllowed, "endpoint only allows GET or DELETE") + } +} + +func (h *NotificationV1) delete(w http.ResponseWriter, r *http.Request) { + ctx := zlog.ContextWithValues(r.Context(), "component", "httptransport/NotificationV1.delete") + path := r.URL.Path + id := filepath.Base(path) + notificationID, err := uuid.Parse(id) + if err != nil { + zlog.Warn(ctx).Err(err).Msg("could not parse notification id") + apiError(w, http.StatusBadRequest, "could not parse notification id: %v", err) + return + } + + err = h.serv.DeleteNotifications(ctx, notificationID) + if err != nil { + zlog.Warn(ctx).Err(err).Msg("could not delete notification") + apiError(w, http.StatusInternalServerError, "could not delete notification: %v", err) + } +} + +// Get will return paginated notifications to the caller. +func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { + ctx := zlog.ContextWithValues(r.Context(), "component", "httptransport/NotificationV1.get") + path := r.URL.Path + id := filepath.Base(path) + notificationID, err := uuid.Parse(id) + if err != nil { + zlog.Warn(ctx).Err(err).Msg("could not parse notification id") + apiError(w, http.StatusBadRequest, "could not parse notification id: %v", err) + return + } + + // optional page_size parameter + var pageSize int + if param := r.URL.Query().Get("page_size"); param != "" { + p, err := strconv.ParseInt(param, 10, 64) + if err != nil { + apiError(w, http.StatusBadRequest, "could not parse %q query param into integer", "page_size") + return + } + pageSize = int(p) + } + if pageSize == 0 { + pageSize = defaultPageSize + } + + // optional page parameter + var next *uuid.UUID + if param := r.URL.Query().Get("next"); param != "" { + n, err := uuid.Parse(param) + if err != nil { + apiError(w, http.StatusBadRequest, "could not parse %q query param into integer", "next") + return + } + if n != uuid.Nil { + next = &n + } + } + + allow := []string{"application/vnd.clair.notification.v1+json", "application/json"} + switch err := pickContentType(w, r, allow); { + case errors.Is(err, nil): // OK + case errors.Is(err, ErrMediaType): + apiError(w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) + return + default: + apiError(w, http.StatusBadRequest, "malformed request: %v", err) + return + } + + inP := ¬ifier.Page{ + Size: pageSize, + Next: next, + } + notifications, outP, err := h.serv.Notifications(ctx, notificationID, inP) + if err != nil { + apiError(w, http.StatusInternalServerError, "failed to retrieve notifications: %v", err) + return + } + + response := notificationResponse{ + Page: outP, + Notifications: notifications, + } + + defer writerError(w, &err)() + enc := codec.GetEncoder(w) + defer codec.PutEncoder(enc) + err = enc.Encode(&response) +} + +func init() { + notificationv1wrapper.init("notificationv1") +} + +var notificationv1wrapper wrapper diff --git a/httptransport/notification_v1_test.go b/httptransport/notification_v1_test.go new file mode 100644 index 0000000000..b81739aa89 --- /dev/null +++ b/httptransport/notification_v1_test.go @@ -0,0 +1,231 @@ +package httptransport + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/quay/zlog" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/trace" + + "github.com/quay/clair/v4/notifier" + "github.com/quay/clair/v4/notifier/service" +) + +// TestUpdateOperationHandler is a parallel harness for testing a UpdateOperation handler. +func TestNotificationsHandler(t *testing.T) { + ctx := zlog.Test(context.Background(), t) + t.Run("Methods", testNotificationsHandlerMethods(ctx)) + t.Run("Get", testNotificationHandlerGet(ctx)) + t.Run("GetParams", testNotificationHandlerGetParams(ctx)) + t.Run("Delete", testNotificationHandlerDelete(ctx)) +} + +var notifierTraceOpt = otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()) + +// testNotificationHandlerDelete confirms the handler performs a delete +// correctly +func testNotificationHandlerDelete(ctx context.Context) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + ctx := zlog.Test(ctx, t) + noteID := uuid.New() + + nm := &service.Mock{ + DeleteNotifications_: func(ctx context.Context, id uuid.UUID) error { + if !cmp.Equal(id, noteID) { + t.Fatalf("got: %v, want: %v", id, noteID) + } + return nil + }, + } + + h, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt) + if err != nil { + t.Error(err) + } + rr := httptest.NewRecorder() + u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) + req := &http.Request{ + URL: u, + Method: http.MethodGet, + } + + h.delete(rr, req) + res := rr.Result() + if res.StatusCode != http.StatusOK { + t.Fatalf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) + } + } +} + +// testNotificationHandlerGet confirms the Get handler works correctly +func testNotificationHandlerGet(ctx context.Context) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + ctx := zlog.Test(ctx, t) + var ( + nextID = uuid.New() + inPageWant = notifier.Page{ + Size: 500, + } + noteID = uuid.New() + outPageWant = notifier.Page{ + Size: 500, + Next: &nextID, + } + ) + + nm := &service.Mock{ + Notifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) { + if !cmp.Equal(id, noteID) { + t.Fatalf("got: %v, wanted: %v", id, noteID) + } + if !cmp.Equal(page, &inPageWant) { + t.Fatalf("got: %v, wanted: %v", page, inPageWant) + } + return []notifier.Notification{}, notifier.Page{ + Size: inPageWant.Size, + Next: &nextID, + }, nil + }, + } + + h, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt) + if err != nil { + t.Error(err) + } + rr := httptest.NewRecorder() + u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) + req := &http.Request{ + URL: u, + Method: http.MethodGet, + } + + h.get(rr, req) + res := rr.Result() + if res.StatusCode != http.StatusOK { + t.Errorf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) + } + var noteResp notificationResponse + if err := json.NewDecoder(res.Body).Decode(¬eResp); err != nil { + t.Errorf("failed to deserialize notification response: %v", err) + } + if !cmp.Equal(noteResp.Page, outPageWant) { + t.Errorf("got: %v, want: %v", noteResp.Page, outPageWant) + } + } +} + +// testNotificationHandlerGetParams confirms the Get handler works correctly +// when parameters are present +func testNotificationHandlerGetParams(ctx context.Context) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + ctx := zlog.Test(ctx, t) + const ( + pageSizeParam = "100" + pageParam = "10" + ) + var ( + nextID = uuid.New() + inPageWant = notifier.Page{ + Size: 100, + } + noteID = uuid.New() + outPageWant = notifier.Page{ + Size: 100, + Next: &nextID, + } + ) + + nm := &service.Mock{ + Notifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) { + if !cmp.Equal(id, noteID) { + t.Fatalf("got: %v, wanted: %v", id, noteID) + } + if !cmp.Equal(page, &inPageWant) { + t.Fatalf("got: %v, wanted: %v", page, inPageWant) + } + return []notifier.Notification{}, notifier.Page{ + Size: inPageWant.Size, + Next: &nextID, + }, nil + }, + } + + h, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt) + if err != nil { + t.Error(err) + } + rr := httptest.NewRecorder() + u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) + v := url.Values{} + v.Set("page_size", pageSizeParam) + v.Set("page", pageParam) + u.RawQuery = v.Encode() + req := &http.Request{ + URL: u, + Method: http.MethodGet, + } + + h.get(rr, req) + res := rr.Result() + if res.StatusCode != http.StatusOK { + t.Errorf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) + } + var noteResp notificationResponse + if err := json.NewDecoder(res.Body).Decode(¬eResp); err != nil { + t.Errorf("failed to deserialize notification response: %v", err) + } + if !cmp.Equal(noteResp.Page, outPageWant) { + t.Errorf("got: %v, want: %v", noteResp.Page, outPageWant) + } + } +} + +func testNotificationsHandlerMethods(ctx context.Context) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + ctx := zlog.Test(ctx, t) + h, err := NewNotificationV1(ctx, `/notifier/api/v1/`, &service.Mock{}, notifierTraceOpt) + if err != nil { + t.Error(err) + } + srv := httptest.NewUnstartedServer(h) + srv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx } + srv.Start() + defer srv.Close() + c := srv.Client() + u := srv.URL + `/notifier/api/v1/notification/` + uuid.Nil.String() + + for _, m := range []string{ + http.MethodConnect, + http.MethodHead, + http.MethodOptions, + http.MethodPatch, + http.MethodPost, + http.MethodPut, + http.MethodTrace, + } { + req, err := http.NewRequest(m, u, nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + resp, err := c.Do(req) + if err != nil { + t.Fatalf("failed to make request: %v", err) + } + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("method: %v got: %v want: %v", m, resp.Status, http.StatusMethodNotAllowed) + } + } + } +} diff --git a/httptransport/notificationshandler.go b/httptransport/notificationshandler.go deleted file mode 100644 index 4d3e0aeabc..0000000000 --- a/httptransport/notificationshandler.go +++ /dev/null @@ -1,151 +0,0 @@ -package httptransport - -import ( - "fmt" - "net/http" - "path/filepath" - "strconv" - - "github.com/google/uuid" - je "github.com/quay/claircore/pkg/jsonerr" - "github.com/quay/zlog" - - "github.com/quay/clair/v4/internal/codec" - "github.com/quay/clair/v4/notifier" -) - -const ( - DefaultPageSize = 500 -) - -type Response struct { - Page notifier.Page `json:"page"` - Notifications []notifier.Notification `json:"notifications"` -} - -type NotifHandler struct { - serv notifier.Service -} - -func NotificationHandler(serv notifier.Service) *NotifHandler { - return &NotifHandler{ - serv: serv, - } -} - -func (h *NotifHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - h.Get(w, r) - case http.MethodDelete: - h.Delete(w, r) - default: - resp := &je.Response{ - Code: "method-not-allowed", - Message: "endpoint only allows GET or DELETE", - } - je.Error(w, resp, http.StatusMethodNotAllowed) - } -} - -func (h *NotifHandler) Delete(w http.ResponseWriter, r *http.Request) { - ctx := zlog.ContextWithValues(r.Context(), "component", "httptransport/NotifHander.Delete") - path := r.URL.Path - id := filepath.Base(path) - notificationID, err := uuid.Parse(id) - if err != nil { - resp := &je.Response{ - Code: "bad-request", - Message: fmt.Sprintf("could not parse notification id: %v", err), - } - zlog.Warn(ctx).Err(err).Msg("could not parse notification id") - je.Error(w, resp, http.StatusBadRequest) - return - } - - err = h.serv.DeleteNotifications(ctx, notificationID) - if err != nil { - resp := &je.Response{ - Code: "internal-server-error", - Message: fmt.Sprintf("could not delete notification: %v", err), - } - zlog.Warn(ctx).Err(err).Msg("could not delete notification") - je.Error(w, resp, http.StatusInternalServerError) - } -} - -// Get will return paginated notifications to the caller. -func (h *NotifHandler) Get(w http.ResponseWriter, r *http.Request) { - ctx := zlog.ContextWithValues(r.Context(), "component", "httptransport/NotifHander.Get") - path := r.URL.Path - id := filepath.Base(path) - notificationID, err := uuid.Parse(id) - if err != nil { - resp := &je.Response{ - Code: "bad-request", - Message: fmt.Sprintf("could not parse notification id: %v", err), - } - zlog.Warn(ctx).Err(err).Msg("could not parse notification id") - je.Error(w, resp, http.StatusBadRequest) - return - } - - // optional page_size parameter - var pageSize int - if param := r.URL.Query().Get("page_size"); param != "" { - p, err := strconv.ParseInt(param, 10, 64) - if err != nil { - resp := &je.Response{ - Code: "bad-request", - Message: "could not parse \"page_size\" query param into integer", - } - je.Error(w, resp, http.StatusBadRequest) - return - } - pageSize = int(p) - } - if pageSize == 0 { - pageSize = DefaultPageSize - } - - // optional page parameter - var next *uuid.UUID - if param := r.URL.Query().Get("next"); param != "" { - n, err := uuid.Parse(param) - if err != nil { - resp := &je.Response{ - Code: "bad-request", - Message: "could not parse \"next\" query param into uuid", - } - je.Error(w, resp, http.StatusBadRequest) - return - } - if n != uuid.Nil { - next = &n - } - } - - inP := ¬ifier.Page{ - Size: pageSize, - Next: next, - } - notifications, outP, err := h.serv.Notifications(ctx, notificationID, inP) - if err != nil { - resp := &je.Response{ - Code: "internal-server-error", - Message: "failed to retrieve notifications: " + err.Error(), - } - je.Error(w, resp, http.StatusInternalServerError) - return - } - - response := Response{ - Page: outP, - Notifications: notifications, - } - - defer writerError(w, &err)() - enc := codec.GetEncoder(w) - defer codec.PutEncoder(enc) - err = enc.Encode(&response) -} diff --git a/httptransport/notificationshandler_test.go b/httptransport/notificationshandler_test.go deleted file mode 100644 index 933aeb0374..0000000000 --- a/httptransport/notificationshandler_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package httptransport - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - - "github.com/quay/clair/v4/notifier" - "github.com/quay/clair/v4/notifier/service" -) - -// TestUpdateOperationHandler is a parallel harness for testing a UpdateOperation handler. -func TestNotificationsHandler(t *testing.T) { - t.Run("Methods", testNotificationsHandlerMethods) - t.Run("Get", testNotificationHandlerGet) - t.Run("GetParams", testNotificationHandlerGetParams) - t.Run("Delete", testNotificationHandlerDelete) -} - -// testNotificationHandlerDelete confirms the handler performs a delete -// correctly -func testNotificationHandlerDelete(t *testing.T) { - t.Parallel() - var ( - noteID = uuid.New() - ) - - nm := &service.Mock{ - DeleteNotifications_: func(ctx context.Context, id uuid.UUID) error { - if !cmp.Equal(id, noteID) { - t.Fatalf("got: %v, want: %v", id, noteID) - } - return nil - }, - } - - h := NotificationHandler(nm) - rr := httptest.NewRecorder() - u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) - req := &http.Request{ - URL: u, - Method: http.MethodGet, - } - - h.Delete(rr, req) - res := rr.Result() - if res.StatusCode != http.StatusOK { - t.Fatalf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) - } -} - -// testNotificationHandlerGetParams confirms the Get handler works correctly -func testNotificationHandlerGet(t *testing.T) { - t.Parallel() - var ( - nextID = uuid.New() - inPageWant = notifier.Page{ - Size: 500, - } - noteID = uuid.New() - outPageWant = notifier.Page{ - Size: 500, - Next: &nextID, - } - ) - - nm := &service.Mock{ - Notifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) { - if !cmp.Equal(id, noteID) { - t.Fatalf("got: %v, wanted: %v", id, noteID) - } - if !cmp.Equal(page, &inPageWant) { - t.Fatalf("got: %v, wanted: %v", page, inPageWant) - } - return []notifier.Notification{}, notifier.Page{ - Size: inPageWant.Size, - Next: &nextID, - }, nil - }, - } - - h := NotificationHandler(nm) - rr := httptest.NewRecorder() - u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) - req := &http.Request{ - URL: u, - Method: http.MethodGet, - } - - h.Get(rr, req) - res := rr.Result() - if res.StatusCode != http.StatusOK { - t.Fatalf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) - } - var noteResp Response - err := json.NewDecoder(res.Body).Decode(¬eResp) - if err != nil { - t.Fatalf("failed to deserialize notification response: %v", err) - } - - if !cmp.Equal(noteResp.Page, outPageWant) { - t.Fatalf("got: %v, want: %v", noteResp.Page, outPageWant) - } -} - -// testNotificationHandlerGetParams confirms the Get handler works correctly -// when parameters are present -func testNotificationHandlerGetParams(t *testing.T) { - t.Parallel() - const ( - pageSizeParam = "100" - pageParam = "10" - ) - var ( - nextID = uuid.New() - inPageWant = notifier.Page{ - Size: 100, - } - noteID = uuid.New() - outPageWant = notifier.Page{ - Size: 100, - Next: &nextID, - } - ) - - nm := &service.Mock{ - Notifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) { - if !cmp.Equal(id, noteID) { - t.Fatalf("got: %v, wanted: %v", id, noteID) - } - if !cmp.Equal(page, &inPageWant) { - t.Fatalf("got: %v, wanted: %v", page, inPageWant) - } - return []notifier.Notification{}, notifier.Page{ - Size: inPageWant.Size, - Next: &nextID, - }, nil - }, - } - - h := NotificationHandler(nm) - rr := httptest.NewRecorder() - u, _ := url.Parse("http://clair-notifier/notifier/api/v1/notification/" + noteID.String()) - v := url.Values{} - v.Set("page_size", pageSizeParam) - v.Set("page", pageParam) - u.RawQuery = v.Encode() - req := &http.Request{ - URL: u, - Method: http.MethodGet, - } - - h.Get(rr, req) - res := rr.Result() - if res.StatusCode != http.StatusOK { - t.Fatalf("got: %v, wanted: %v", res.StatusCode, http.StatusOK) - } - var noteResp Response - err := json.NewDecoder(res.Body).Decode(¬eResp) - if err != nil { - t.Fatalf("failed to deserialize notification response: %v", err) - } - - if !cmp.Equal(noteResp.Page, outPageWant) { - t.Fatalf("got: %v, want: %v", noteResp.Page, outPageWant) - } -} - -func testNotificationsHandlerMethods(t *testing.T) { - t.Parallel() - h := NotificationHandler(&service.Mock{}) - srv := httptest.NewServer(h) - defer srv.Close() - c := srv.Client() - - for _, m := range []string{ - http.MethodConnect, - http.MethodHead, - http.MethodOptions, - http.MethodPatch, - http.MethodPost, - http.MethodPut, - http.MethodTrace, - } { - req, err := http.NewRequest(m, srv.URL, nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - resp, err := c.Do(req) - if err != nil { - t.Fatalf("failed to make request: %v", err) - } - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Fatalf("method: %v got: %v want: %v", m, resp.Status, http.StatusMethodNotAllowed) - } - } -} diff --git a/httptransport/server.go b/httptransport/server.go index 02f346ac26..f579000853 100644 --- a/httptransport/server.go +++ b/httptransport/server.go @@ -208,21 +208,16 @@ func (t *Server) configureMatcherMode(ctx context.Context) error { // // This mode runs only a Notifier in a single process. func (t *Server) configureNotifierMode(ctx context.Context) error { - // requires both an indexer and matcher service. indexer service - // is assumed to be a remote call over the network if t.notifier == nil { return clairerror.ErrNotInitialized{Msg: "NotifierMode requires a notifier service"} } + prefix := notifierRoot + apiRoot + v1, err := NewNotificationV1(ctx, prefix, t.notifier, t.traceOpt) + if err != nil { + return fmt.Errorf("notifier configuration: %w", err) + } - t.Handle(NotificationAPIPath, - intromw.InstrumentedHandler(NotificationAPIPath, t.traceOpt, NotificationHandler(t.notifier))) - - t.Handle(KeysAPIPath, - intromw.InstrumentedHandler(KeysAPIPath, t.traceOpt, gone)) - - t.Handle(KeyByIDAPIPath, - intromw.InstrumentedHandler(KeyByIDAPIPath+"_KEY", t.traceOpt, gone)) - + t.Handle(prefix, v1) return nil }