Skip to content

Commit

Permalink
Add support for serving the static assets in a report
Browse files Browse the repository at this point in the history
  • Loading branch information
bcspragu committed Jan 13, 2024
1 parent 69a9def commit bedc664
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 8 deletions.
1 change: 1 addition & 0 deletions cmd/server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"//dockertask",
"//oapierr",
"//openapi:pacta_generated",
"//reportsrv",
"//secrets",
"//session",
"//task",
Expand Down
32 changes: 24 additions & 8 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/RMI/pacta/dockertask"
"github.com/RMI/pacta/oapierr"
oapipacta "github.com/RMI/pacta/openapi/pacta"
"github.com/RMI/pacta/reportsrv"
"github.com/RMI/pacta/secrets"
"github.com/RMI/pacta/session"
"github.com/RMI/pacta/task"
Expand Down Expand Up @@ -286,18 +287,24 @@ func run(args []string) error {
return fmt.Errorf("failed to init Azure Event Grid handler: %w", err)
}

r := chi.NewRouter()
r.Group(eventSrv.RegisterHandlers)
reportSrv, err := reportsrv.New(&reportsrv.Config{
DB: db,
Blob: blobClient,
Logger: logger,
})
if err != nil {
return fmt.Errorf("failed to init report server: %w", err)
}

jwKey, err := jwk.FromRaw(sec.AuthVerificationKey.PublicKey)
if err != nil {
return fmt.Errorf("failed to make JWK key: %w", err)
}
jwKey.Set(jwk.KeyIDKey, sec.AuthVerificationKey.ID)

// We now register our PACTA above as the handler for the interface
oapipacta.HandlerWithOptions(pactaStrictHandler, oapipacta.ChiServerOptions{
BaseRouter: r.With(
type middlewareFunc = func(http.Handler) http.Handler
middleware := func(addl ...middlewareFunc) []middlewareFunc {
return append([]middlewareFunc{
// The order of these is important. We run RequestID and RealIP first to
// populate relevant metadata for logging, and we run recovery immediately after
// logging so it can catch any subsequent panics, but still has access to the
Expand All @@ -306,14 +313,23 @@ func run(args []string) error {
chimiddleware.RealIP,
zaphttplog.NewMiddleware(logger, zaphttplog.WithConcise(false)),
chimiddleware.Recoverer,

jwtauth.Verifier(jwtauth.New("EdDSA", nil, jwKey)),
jwtauth.Authenticator,
session.WithAuthn(logger, db),
}, addl...)
}

oapimiddleware.OapiRequestValidator(pactaSwagger),
r := chi.NewRouter()
r.With(chimiddleware.Recoverer).Group(eventSrv.RegisterHandlers)
r.With(middleware()...).Group(reportSrv.RegisterHandlers)

rateLimitMiddleware(*rateLimitMaxRequests, *rateLimitUnitTime),
// We now register our PACTA above as the handler for the interface
oapipacta.HandlerWithOptions(pactaStrictHandler, oapipacta.ChiServerOptions{
BaseRouter: r.With(
middleware(
oapimiddleware.OapiRequestValidator(pactaSwagger),
rateLimitMiddleware(*rateLimitMaxRequests, *rateLimitUnitTime),
)...,
),
})

Expand Down
15 changes: 15 additions & 0 deletions reportsrv/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "reportsrv",
srcs = ["reportsrv.go"],
importpath = "github.com/RMI/pacta/reportsrv",
visibility = ["//visibility:public"],
deps = [
"//blob",
"//db",
"//pacta",
"@com_github_go_chi_chi_v5//:chi",
"@org_uber_go_zap//:zap",
],
)
177 changes: 177 additions & 0 deletions reportsrv/reportsrv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package reportsrv

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/RMI/pacta/blob"
"github.com/RMI/pacta/db"
"github.com/RMI/pacta/pacta"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)

type Config struct {
DB DB
Blob Blob
Logger *zap.Logger
}

func (c *Config) validate() error {
if c.DB == nil {
return errors.New("no DB was given")
}
if c.Blob == nil {
return errors.New("no blob client was given")
}
if c.Logger == nil {
return errors.New("no logger was given")
}
return nil
}

type Server struct {
db DB
blob Blob
logger *zap.Logger
}

type DB interface {
Begin(context.Context) (db.Tx, error)
NoTxn(context.Context) db.Tx
Transactional(context.Context, func(tx db.Tx) error) error
RunOrContinueTransaction(db.Tx, func(tx db.Tx) error) error

AnalysisArtifactsForAnalysis(tx db.Tx, id pacta.AnalysisID) ([]*pacta.AnalysisArtifact, error)
Blobs(tx db.Tx, ids []pacta.BlobID) (map[pacta.BlobID]*pacta.Blob, error)
}

type Blob interface {
Scheme() blob.Scheme

ReadBlob(ctx context.Context, uri string) (io.ReadCloser, error)
}

func New(cfg *Config) (*Server, error) {
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &Server{
db: cfg.DB,
blob: cfg.Blob,
logger: cfg.Logger,
}, nil
}

func (s *Server) verifyRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO!
next.ServeHTTP(w, r)
})
}

func (s *Server) RegisterHandlers(r chi.Router) {
r.Use(s.verifyRequest)
r.Get("/report/{analysis_id}/*", s.serveReport)
}

func (s *Server) serveReport(w http.ResponseWriter, r *http.Request) {
aID := pacta.AnalysisID(chi.URLParam(r, "analysis_id"))
if aID == "" {
http.Error(w, "no id given", http.StatusBadRequest)
return
}
ctx := r.Context()

artifacts, err := s.db.AnalysisArtifactsForAnalysis(s.db.NoTxn(ctx), aID)
if err != nil {
http.Error(w, "failed to get artifacts: "+err.Error(), http.StatusInternalServerError)
return
}
var blobIDs []pacta.BlobID
for _, a := range artifacts {
blobIDs = append(blobIDs, a.Blob.ID)
}

blobs, err := s.db.Blobs(s.db.NoTxn(ctx), blobIDs)
if err != nil {
http.Error(w, "failed to get blobs: "+err.Error(), http.StatusInternalServerError)
return
}

subPath := strings.TrimPrefix(r.URL.Path, "/report/"+string(aID)+"/")
if subPath == "" {
subPath = "index.html"
}

for _, a := range artifacts {
// Container is just 'reports', we can ignore that.
b, ok := blobs[a.Blob.ID]
if !ok {
s.logger.Error("no blob loaded for blob ID", zap.String("analysis_artifact_id", string(a.ID)), zap.String("blob_id", string(a.Blob.ID)))
continue
}
uri := string(b.BlobURI)
_, path, ok := blob.SplitURI(s.blob.Scheme(), uri)
if !ok {
s.logger.Error("blob had invalid URI", zap.String("analysis_artifact_id", string(a.ID)), zap.String("blob_uri", uri))
continue
}
_, uriPath, ok := strings.Cut(path, "/")
if !ok {
s.logger.Error("path had no UUID prefix", zap.String("analysis_artifact_id", string(a.ID)), zap.String("blob_uri", uri), zap.String("blob_path", path))
continue
}

if uriPath != subPath {
continue
}

r, err := s.blob.ReadBlob(ctx, uri)
if err != nil {
http.Error(w, "failed to read blob: "+err.Error(), http.StatusInternalServerError)
return
}
defer r.Close()
w.Header().Set("Content-Type", fileTypeToMIME(b.FileType))
if _, err := io.Copy(w, r); err != nil {
http.Error(w, "failed to read/write blob: "+err.Error(), http.StatusInternalServerError)
return
}
return
}
http.Error(w, "not found", http.StatusNotFound)
return
}

func fileTypeToMIME(ft pacta.FileType) string {
switch ft {
case pacta.FileType_CSV:
return "text/csv"
case pacta.FileType_YAML:
// Note: This one is actually kinda contentious, but I don't think it matters
// much. See https://stackoverflow.com/q/332129
return "text/yaml"
case pacta.FileType_ZIP:
return "application/zip"
case pacta.FileType_HTML:
return "text/html"
case pacta.FileType_JSON:
return "application/json"
case pacta.FileType_TEXT:
return "text/plain"
case pacta.FileType_CSS:
return "text/css"
case pacta.FileType_JS:
return "text/javascript"
case pacta.FileType_TTF:
return "font/ttf"
default:
return "application/octet-stream"
}

}

0 comments on commit bedc664

Please sign in to comment.