diff --git a/httptransport/concurrentlimit.go b/httptransport/concurrentlimit.go index 025985b40f..06872835c2 100644 --- a/httptransport/concurrentlimit.go +++ b/httptransport/concurrentlimit.go @@ -38,14 +38,15 @@ func (l *limitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if sem != nil { if !sem.TryAcquire(1) { concurrentLimitedCounter.WithLabelValues(endpt, r.Method).Add(1) - zlog.Info(r.Context()). + ctx := r.Context() + zlog.Info(ctx). Str("remote_addr", r.RemoteAddr). Str("method", r.Method). Str("request_uri", r.RequestURI). Int("status", http.StatusTooManyRequests). Msg("rate limited HTTP request") - apiError(w, http.StatusTooManyRequests, "server handling too many requests") + apiError(ctx, w, http.StatusTooManyRequests, "server handling too many requests") return } defer sem.Release(1) diff --git a/httptransport/discoveryhandler.go b/httptransport/discoveryhandler.go index 8944760f95..dc7e5d442d 100644 --- a/httptransport/discoveryhandler.go +++ b/httptransport/discoveryhandler.go @@ -21,17 +21,18 @@ var ( func DiscoveryHandler() http.Handler { allow := []string{`application/json`, `application/vnd.oai.openapi+json`} return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() if r.Method != http.MethodGet { - apiError(w, http.StatusMethodNotAllowed, "endpoint only allows GET") + apiError(ctx, w, http.StatusMethodNotAllowed, "endpoint only allows GET") return } switch err := pickContentType(w, r, allow); { case errors.Is(err, nil): case errors.Is(err, ErrMediaType): - apiError(w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) + apiError(ctx, w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) return default: - apiError(w, http.StatusInternalServerError, "unexpected error: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "unexpected error: %v", err) return } w.Header().Set("etag", openapiJSONEtag) diff --git a/httptransport/discoveryhandler_test.go b/httptransport/discoveryhandler_test.go index 8024132198..50c28f5dd4 100644 --- a/httptransport/discoveryhandler_test.go +++ b/httptransport/discoveryhandler_test.go @@ -2,6 +2,7 @@ package httptransport import ( "bytes" + "context" "encoding/json" "io/ioutil" "net/http" @@ -14,13 +15,15 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/quay/zlog" ) func TestDiscoveryEndpoint(t *testing.T) { + ctx := zlog.Test(context.Background(), t) h := DiscoveryHandler() r := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/openapi/v1", nil) + req := httptest.NewRequest("GET", "/openapi/v1", nil).WithContext(ctx) req.Header.Set("Accept", "application/yaml, application/json; q=0.4, application/vnd.oai.openapi+json; q=1.0") h.ServeHTTP(r, req) @@ -50,10 +53,11 @@ func TestDiscoveryEndpoint(t *testing.T) { } func TestDiscoveryFailure(t *testing.T) { + ctx := zlog.Test(context.Background(), t) h := DiscoveryHandler() r := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/openapi/v1", nil) + req := httptest.NewRequest("GET", "/openapi/v1", nil).WithContext(ctx) req.Header.Set("Accept", "application/yaml") h.ServeHTTP(r, req) diff --git a/httptransport/error.go b/httptransport/error.go index 47de2c6aae..e36c7e1012 100644 --- a/httptransport/error.go +++ b/httptransport/error.go @@ -2,14 +2,17 @@ package httptransport import ( "bytes" + "context" "encoding/json" "fmt" "net/http" + + "github.com/quay/zlog" ) // ApiError writes an untyped (that is, "application/json") error with the // provided HTTP status code and message. -func apiError(w http.ResponseWriter, code int, f string, v ...interface{}) { +func apiError(ctx context.Context, w http.ResponseWriter, code int, f string, v ...interface{}) { const errheader = `Clair-Error` h := w.Header() h.Del("link") @@ -17,6 +20,12 @@ func apiError(w http.ResponseWriter, code int, f string, v ...interface{}) { h.Set("x-content-type-options", "nosniff") h.Set("trailer", errheader) w.WriteHeader(code) + if ev := zlog.Debug(ctx); ev.Enabled() { + ev. + Int("code", code). + Str("error", fmt.Sprintf(f, v...)). + Msg("http error response") + } var buf bytes.Buffer buf.WriteString(`{"code":"`) diff --git a/httptransport/indexer_v1.go b/httptransport/indexer_v1.go index ed60937038..9106e152bc 100644 --- a/httptransport/indexer_v1.go +++ b/httptransport/indexer_v1.go @@ -73,14 +73,14 @@ func (h *IndexerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() switch r.Method { case http.MethodPost: case http.MethodDelete: default: - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } - ctx := r.Context() defer r.Body.Close() dec := codec.GetDecoder(r.Body) defer codec.PutDecoder(dec) @@ -88,16 +88,16 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) { case http.MethodPost: state, err := h.srv.State(ctx) if err != nil { - apiError(w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) return } var m claircore.Manifest if err := dec.Decode(&m); err != nil { - apiError(w, http.StatusBadRequest, "failed to deserialize manifest: %v", err) + apiError(ctx, w, http.StatusBadRequest, "failed to deserialize manifest: %v", err) return } if m.Hash.String() == "" || len(m.Layers) == 0 { - apiError(w, http.StatusBadRequest, "bogus manifest") + apiError(ctx, w, http.StatusBadRequest, "bogus manifest") return } next := path.Join(r.URL.Path, m.Hash.String()) @@ -116,10 +116,10 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) { switch { case errors.Is(err, nil): case errors.Is(err, tarfs.ErrFormat): - apiError(w, http.StatusBadRequest, "failed to start scan: %v", err) + apiError(ctx, w, http.StatusBadRequest, "failed to start scan: %v", err) return default: - apiError(w, http.StatusInternalServerError, "failed to start scan: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "failed to start scan: %v", err) return } @@ -133,12 +133,12 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: var ds []claircore.Digest if err := dec.Decode(&ds); err != nil { - apiError(w, http.StatusBadRequest, "failed to deserialize bulk delete: %v", err) + apiError(ctx, w, http.StatusBadRequest, "failed to deserialize bulk delete: %v", err) return } ds, err := h.srv.DeleteManifests(ctx, ds...) if err != nil { - apiError(w, http.StatusInternalServerError, "could not delete manifests: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not delete manifests: %v", err) return } zlog.Debug(ctx). @@ -158,35 +158,35 @@ const ( ) func (h *IndexerV1) indexReportOne(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() switch r.Method { case http.MethodGet: case http.MethodDelete: default: - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } d, err := getDigest(w, r) if err != nil { - apiError(w, http.StatusBadRequest, "malformed path: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed path: %v", err) return } - ctx := r.Context() switch r.Method { case http.MethodGet: allow := []string{"application/vnd.clair.indexreport.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) + apiError(ctx, w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) return default: - apiError(w, http.StatusBadRequest, "malformed request: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed request: %v", err) return } state, err := h.srv.State(ctx) if err != nil { - apiError(w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) return } validator := `"` + state + `"` @@ -197,11 +197,11 @@ func (h *IndexerV1) indexReportOne(w http.ResponseWriter, r *http.Request) { report, ok, err := h.srv.IndexReport(ctx, d) if !ok { - apiError(w, http.StatusNotFound, "index report not found") + apiError(ctx, w, http.StatusNotFound, "index report not found") return } if err != nil { - apiError(w, http.StatusInternalServerError, "could not retrieve index report: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not retrieve index report: %v", err) return } @@ -212,7 +212,7 @@ func (h *IndexerV1) indexReportOne(w http.ResponseWriter, r *http.Request) { err = enc.Encode(report) case http.MethodDelete: if _, err := h.srv.DeleteManifests(ctx, d); err != nil { - apiError(w, http.StatusInternalServerError, "unable to delete manifest: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "unable to delete manifest: %v", err) return } w.WriteHeader(http.StatusNoContent) @@ -220,24 +220,24 @@ func (h *IndexerV1) indexReportOne(w http.ResponseWriter, r *http.Request) { } func (h *IndexerV1) indexState(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() if r.Method != http.MethodGet { - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } allow := []string{"application/vnd.clair.indexstate.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) + apiError(ctx, w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) return default: - apiError(w, http.StatusBadRequest, "malformed request: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed request: %v", err) return } - ctx := r.Context() s, err := h.srv.State(ctx) if err != nil { - apiError(w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not retrieve indexer state: %v", err) return } @@ -261,21 +261,21 @@ func (h *IndexerV1) indexState(w http.ResponseWriter, r *http.Request) { } func (h *IndexerV1) affectedManifests(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() if r.Method != http.MethodPost { - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } allow := []string{"application/vnd.clair.affectedmanifests.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) + apiError(ctx, w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) return default: - apiError(w, http.StatusBadRequest, "malformed request: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed request: %v", err) return } - ctx := r.Context() var vulnerabilities struct { V []claircore.Vulnerability `json:"vulnerabilities"` @@ -283,13 +283,13 @@ func (h *IndexerV1) affectedManifests(w http.ResponseWriter, r *http.Request) { dec := codec.GetDecoder(r.Body) defer codec.PutDecoder(dec) if err := dec.Decode(&vulnerabilities); err != nil { - apiError(w, http.StatusBadRequest, "failed to deserialize vulnerabilities: %v", err) + apiError(ctx, w, http.StatusBadRequest, "failed to deserialize vulnerabilities: %v", err) return } affected, err := h.srv.AffectedManifests(ctx, vulnerabilities.V) if err != nil { - apiError(w, http.StatusInternalServerError, "could not retrieve affected manifests: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not retrieve affected manifests: %v", err) return } diff --git a/httptransport/matcher_v1.go b/httptransport/matcher_v1.go index d2b9e124ec..fdc4b94f69 100644 --- a/httptransport/matcher_v1.go +++ b/httptransport/matcher_v1.go @@ -85,7 +85,7 @@ func (h *MatcherV1) vulnerabilityReport(w http.ResponseWriter, r *http.Request) "component", "httptransport/MatcherV1.vulnerabilityReport") if r.Method != http.MethodGet { - apiError(w, http.StatusMethodNotAllowed, "endpoint only allows GET") + apiError(ctx, w, http.StatusMethodNotAllowed, "endpoint only allows GET") return } ctx, done := context.WithCancel(ctx) @@ -94,18 +94,18 @@ func (h *MatcherV1) vulnerabilityReport(w http.ResponseWriter, r *http.Request) manifestStr := path.Base(r.URL.Path) if manifestStr == "" { - apiError(w, http.StatusBadRequest, "malformed path. provide a single manifest hash") + apiError(ctx, w, http.StatusBadRequest, "malformed path. provide a single manifest hash") return } manifest, err := claircore.ParseDigest(manifestStr) if err != nil { - apiError(w, http.StatusBadRequest, "malformed path: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed path: %v", err) return } initd, err := h.srv.Initialized(ctx) if err != nil { - apiError(w, http.StatusInternalServerError, err.Error()) + apiError(ctx, w, http.StatusInternalServerError, err.Error()) return } if !initd { @@ -116,18 +116,18 @@ func (h *MatcherV1) vulnerabilityReport(w http.ResponseWriter, r *http.Request) indexReport, ok, err := h.indexerSrv.IndexReport(ctx, manifest) // check err first if err != nil { - apiError(w, http.StatusInternalServerError, "experienced a server side error: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "experienced a server side error: %v", err) return } // now check bool only after confirming no err if !ok { - apiError(w, http.StatusNotFound, "index report for manifest %q not found", manifest.String()) + apiError(ctx, w, http.StatusNotFound, "index report for manifest %q not found", manifest.String()) return } vulnReport, err := h.srv.Scan(ctx, indexReport) if err != nil { - apiError(w, http.StatusInternalServerError, "failed to start scan: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "failed to start scan: %v", err) return } @@ -145,7 +145,7 @@ func (h *MatcherV1) updateDiffHandler(w http.ResponseWriter, r *http.Request) { "component", "httptransport/MatcherV1.updateDiffHandler") if r.Method != http.MethodGet { - apiError(w, http.StatusMethodNotAllowed, "endpoint only allows GET") + apiError(ctx, w, http.StatusMethodNotAllowed, "endpoint only allows GET") return } // prev param is optional. @@ -154,7 +154,7 @@ func (h *MatcherV1) updateDiffHandler(w http.ResponseWriter, r *http.Request) { if param := r.URL.Query().Get("prev"); param != "" { prev, err = uuid.Parse(param) if err != nil { - apiError(w, http.StatusBadRequest, "could not parse \"prev\" query param into uuid") + apiError(ctx, w, http.StatusBadRequest, "could not parse \"prev\" query param into uuid") return } } @@ -163,17 +163,17 @@ func (h *MatcherV1) updateDiffHandler(w http.ResponseWriter, r *http.Request) { var cur uuid.UUID var param string if param = r.URL.Query().Get("cur"); param == "" { - apiError(w, http.StatusBadRequest, "\"cur\" query param is required") + apiError(ctx, w, http.StatusBadRequest, "\"cur\" query param is required") return } if cur, err = uuid.Parse(param); err != nil { - apiError(w, http.StatusBadRequest, "could not parse \"cur\" query param into uuid") + apiError(ctx, w, http.StatusBadRequest, "could not parse \"cur\" query param into uuid") return } diff, err := h.srv.UpdateDiff(ctx, prev, cur) if err != nil { - apiError(w, http.StatusInternalServerError, "could not get update operations: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not get update operations: %v", err) return } @@ -190,7 +190,7 @@ func (h *MatcherV1) updateOperationHandlerGet(w http.ResponseWriter, r *http.Req switch r.Method { case http.MethodGet: default: - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } @@ -201,7 +201,7 @@ func (h *MatcherV1) updateOperationHandlerGet(w http.ResponseWriter, r *http.Req case "", "vulnerability": // Leave as default default: - apiError(w, http.StatusBadRequest, "unknown kind: %q", k) + apiError(ctx, w, http.StatusBadRequest, "unknown kind: %q", k) return } @@ -225,7 +225,7 @@ func (h *MatcherV1) updateOperationHandlerGet(w http.ResponseWriter, r *http.Req uos, err = h.srv.UpdateOperations(ctx, kind) } if err != nil { - apiError(w, http.StatusInternalServerError, "could not get update operations: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not get update operations: %v", err) return } @@ -241,7 +241,7 @@ func (h *MatcherV1) updateOperationHandlerDelete(w http.ResponseWriter, r *http. switch r.Method { case http.MethodDelete: default: - apiError(w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) + apiError(ctx, w, http.StatusMethodNotAllowed, "method disallowed: %s", r.Method) return } @@ -250,13 +250,13 @@ func (h *MatcherV1) updateOperationHandlerDelete(w http.ResponseWriter, r *http. uuid, err := uuid.Parse(id) if err != nil { zlog.Warn(ctx).Err(err).Msg("could not deserialize manifest") - apiError(w, http.StatusBadRequest, "could not deserialize manifest: %v", err) + apiError(ctx, w, http.StatusBadRequest, "could not deserialize manifest: %v", err) return } _, err = h.srv.DeleteUpdateOperations(ctx, uuid) if err != nil { - apiError(w, http.StatusInternalServerError, "could not get update operations: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "could not get update operations: %v", err) return } } diff --git a/httptransport/notification_v1.go b/httptransport/notification_v1.go index 513f208a2d..5f6593a487 100644 --- a/httptransport/notification_v1.go +++ b/httptransport/notification_v1.go @@ -79,7 +79,7 @@ func (h *NotificationV1) serveHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: h.delete(w, r) default: - apiError(w, http.StatusMethodNotAllowed, "endpoint only allows GET or DELETE") + apiError(r.Context(), w, http.StatusMethodNotAllowed, "endpoint only allows GET or DELETE") } } @@ -90,14 +90,14 @@ func (h *NotificationV1) delete(w http.ResponseWriter, r *http.Request) { 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) + apiError(ctx, 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) + apiError(ctx, w, http.StatusInternalServerError, "could not delete notification: %v", err) } } @@ -109,7 +109,7 @@ func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { 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) + apiError(ctx, w, http.StatusBadRequest, "could not parse notification id: %v", err) return } @@ -118,7 +118,7 @@ func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { 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") + apiError(ctx, w, http.StatusBadRequest, "could not parse %q query param into integer", "page_size") return } pageSize = int(p) @@ -132,7 +132,7 @@ func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { 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") + apiError(ctx, w, http.StatusBadRequest, "could not parse %q query param into integer", "next") return } if n != uuid.Nil { @@ -144,10 +144,10 @@ func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { 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) + apiError(ctx, w, http.StatusUnsupportedMediaType, "unable to negotiate common media type for %v", allow) return default: - apiError(w, http.StatusBadRequest, "malformed request: %v", err) + apiError(ctx, w, http.StatusBadRequest, "malformed request: %v", err) return } @@ -157,7 +157,7 @@ func (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) { } notifications, outP, err := h.serv.Notifications(ctx, notificationID, inP) if err != nil { - apiError(w, http.StatusInternalServerError, "failed to retrieve notifications: %v", err) + apiError(ctx, w, http.StatusInternalServerError, "failed to retrieve notifications: %v", err) return }