Skip to content

Commit

Permalink
httptransport: add Cache-Control header to VulnerabilityReport response
Browse files Browse the repository at this point in the history
This should allow clients to talk to Clair though a caching proxy and
transparently reduce load on matchers.

Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Aug 11, 2021
1 parent 1d6ce96 commit 22a2548
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 80 deletions.
18 changes: 17 additions & 1 deletion httptransport/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package httptransport

import (
"context"
"fmt"
"net"
"net/http"
"time"

"github.com/quay/zlog"
othttp "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
Expand Down Expand Up @@ -192,8 +194,14 @@ func (t *Server) configureMatcherMode(_ context.Context) error {
return clairerror.ErrNotInitialized{Msg: "MatcherMode requires both indexer and matcher services"}
}

reportHandler := &VulnerabilityReportHandler{
Indexer: t.indexer,
Matcher: t.matcher,
Cache: t.conf.Matcher.CacheAge,
}

t.Handle(VulnerabilityReportPath,
intromw.InstrumentedHandler(VulnerabilityReportPath, t.traceOpt, VulnerabilityReportHandler(t.matcher, t.indexer)))
intromw.InstrumentedHandler(VulnerabilityReportPath, t.traceOpt, reportHandler))

t.Handle(UpdateOperationAPIPath,
intromw.InstrumentedHandler(UpdateOperationAPIPath, t.traceOpt, UpdateOperationHandler(t.matcher)))
Expand Down Expand Up @@ -277,3 +285,11 @@ func writerError(w http.ResponseWriter, e *error) func() {
w.Header().Add(errHeader, (*e).Error())
}
}

// SetCacheControl sets the "Cache-Control" header on the response.
func setCacheControl(w http.ResponseWriter, age time.Duration) {
// The odd format string means "print float as wide as needed and to 0
// precision."
const f = `max-age=%.f`
w.Header().Set("cache-control", fmt.Sprintf(f, age.Seconds()))
}
156 changes: 82 additions & 74 deletions httptransport/vulnerabilityreporthandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptrace"
"path"
"time"

"github.com/quay/claircore"
je "github.com/quay/claircore/pkg/jsonerr"
Expand All @@ -16,91 +17,98 @@ import (
"github.com/quay/clair/v4/matcher"
)

// VulnerabilityReportHandler utilizes a Service to serialize
// and return a claircore.VulnerabilityReport
func VulnerabilityReportHandler(service matcher.Service, indexer indexer.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
resp := &je.Response{
Code: "method-not-allowed",
Message: "endpoint only allows GET",
}
je.Error(w, resp, http.StatusMethodNotAllowed)
return
}
ctx, done := context.WithCancel(r.Context())
defer done()
ctx = httptrace.WithClientTrace(ctx, oteltrace.NewClientTrace(ctx))
// VulnerabilityReportHandler produces VulnerabilityReports.
type VulnerabilityReportHandler struct {
Matcher matcher.Service
Indexer indexer.Service
Cache time.Duration
}

manifestStr := path.Base(r.URL.Path)
if manifestStr == "" {
resp := &je.Response{
Code: "bad-request",
Message: "malformed path. provide a single manifest hash",
}
je.Error(w, resp, http.StatusBadRequest)
return
}
manifest, err := claircore.ParseDigest(manifestStr)
if err != nil {
resp := &je.Response{
Code: "bad-request",
Message: "malformed path: " + err.Error(),
}
je.Error(w, resp, http.StatusBadRequest)
return
var _ http.Handler = (*VulnerabilityReportHandler)(nil)

// ServeHTTP implements http.Handler.
func (h *VulnerabilityReportHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
resp := &je.Response{
Code: "method-not-allowed",
Message: "endpoint only allows GET",
}
je.Error(w, resp, http.StatusMethodNotAllowed)
return
}
ctx, done := context.WithCancel(r.Context())
defer done()
ctx = httptrace.WithClientTrace(ctx, oteltrace.NewClientTrace(ctx))

initd, err := service.Initialized(ctx)
if err != nil {
resp := &je.Response{
Code: "internal-server-error",
Message: err.Error(),
}
je.Error(w, resp, http.StatusInternalServerError)
return
manifestStr := path.Base(r.URL.Path)
if manifestStr == "" {
resp := &je.Response{
Code: "bad-request",
Message: "malformed path. provide a single manifest hash",
}
if !initd {
w.WriteHeader(http.StatusAccepted)
return
je.Error(w, resp, http.StatusBadRequest)
return
}
manifest, err := claircore.ParseDigest(manifestStr)
if err != nil {
resp := &je.Response{
Code: "bad-request",
Message: "malformed path: " + err.Error(),
}
je.Error(w, resp, http.StatusBadRequest)
return
}

indexReport, ok, err := indexer.IndexReport(ctx, manifest)
// check err first
if err != nil {
resp := &je.Response{
Code: "internal-server-error",
Message: fmt.Sprintf("experienced a server side error: %v", err),
}
je.Error(w, resp, http.StatusInternalServerError)
return
initd, err := h.Matcher.Initialized(ctx)
if err != nil {
resp := &je.Response{
Code: "internal-server-error",
Message: err.Error(),
}
// now check bool only after confirming no err
if !ok {
resp := &je.Response{
Code: "not-found",
Message: fmt.Sprintf("index report for manifest %q not found", manifest.String()),
}
je.Error(w, resp, http.StatusNotFound)
return
je.Error(w, resp, http.StatusInternalServerError)
return
}
if !initd {
w.WriteHeader(http.StatusAccepted)
return
}

indexReport, ok, err := h.Indexer.IndexReport(ctx, manifest)
// check err first
if err != nil {
resp := &je.Response{
Code: "internal-server-error",
Message: fmt.Sprintf("experienced a server side error: %v", err),
}

vulnReport, err := service.Scan(ctx, indexReport)
if err != nil {
resp := &je.Response{
Code: "match-error",
Message: fmt.Sprintf("failed to start scan: %v", err),
}
je.Error(w, resp, http.StatusInternalServerError)
return
je.Error(w, resp, http.StatusInternalServerError)
return
}
// now check bool only after confirming no err
if !ok {
resp := &je.Response{
Code: "not-found",
Message: fmt.Sprintf("index report for manifest %q not found", manifest.String()),
}
je.Error(w, resp, http.StatusNotFound)
return

w.Header().Set("content-type", "application/json")
}

defer writerError(w, &err)()
enc := codec.GetEncoder(w)
defer codec.PutEncoder(enc)
err = enc.Encode(vulnReport)
vulnReport, err := h.Matcher.Scan(ctx, indexReport)
if err != nil {
resp := &je.Response{
Code: "match-error",
Message: fmt.Sprintf("failed to start scan: %v", err),
}
je.Error(w, resp, http.StatusInternalServerError)
return
}

w.Header().Set("content-type", "application/json")
setCacheControl(w, h.Cache)

defer writerError(w, &err)()
enc := codec.GetEncoder(w)
defer codec.PutEncoder(enc)
err = enc.Encode(vulnReport)
}
10 changes: 5 additions & 5 deletions httptransport/vulnerabilityreporthandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ import (
func TestInitialized(t *testing.T) {
var initd bool
digest := claircore.MustParseDigest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
h := VulnerabilityReportHandler(
&matcher.Mock{
h := &VulnerabilityReportHandler{
Matcher: &matcher.Mock{
Initialized_: func(_ context.Context) (bool, error) {
return initd, nil
},
},
&indexer.Mock{
IndexReport_: func(ctx context.Context, d claircore.Digest) (*claircore.IndexReport, bool, error) {
Indexer: &indexer.Mock{
IndexReport_: func(_ context.Context, d claircore.Digest) (*claircore.IndexReport, bool, error) {
if got, want := d.String(), digest.String(); got != want {
return nil, false, fmt.Errorf("unexpected digest: %v", got)
}
return nil, false, nil
},
},
)
}
srv := httptest.NewServer(h)
defer srv.Close()
c := srv.Client()
Expand Down

0 comments on commit 22a2548

Please sign in to comment.