Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ linters:
# Such cases aren't reported by default.
# Default: false
check-type-assertions: true
check-blank: true

exhaustive:
# Program elements to check for exhaustiveness.
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func Load(ctx context.Context, r io.Reader, mux *http.ServeMux, vars map[string]
}

func expandVars(ast *hcl.AST, vars map[string]string) {
_ = hcl.Visit(ast, func(node hcl.Node, next func() error) error {
_ = hcl.Visit(ast, func(node hcl.Node, next func() error) error { //nolint:errcheck
attr, ok := node.(*hcl.Attribute)
if ok {
switch attr := attr.Value.(type) {
Expand Down
16 changes: 13 additions & 3 deletions internal/httputil/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@ func ErrorResponse(w http.ResponseWriter, r *http.Request, status int, msg strin
http.Error(w, msg, status)
}

// HTTPResponder is an error that knows how to write itself as an HTTP response.
type HTTPResponder interface {
error
WriteHTTP(http.ResponseWriter, *http.Request)
}

type HTTPError struct {
status int
err error
}

func (h HTTPError) Error() string { return fmt.Sprintf("%d: %s", h.status, h.err) }
func (h HTTPError) Unwrap() error { return h.err }
func (h HTTPError) StatusCode() int { return h.status }
func (h HTTPError) Error() string { return fmt.Sprintf("%d: %s", h.status, h.err) }
func (h HTTPError) Unwrap() error { return h.err }

// WriteHTTP writes this error as an HTTP response.
func (h HTTPError) WriteHTTP(w http.ResponseWriter, r *http.Request) {
ErrorResponse(w, r, h.status, h.err.Error())
}

func Errorf(status int, format string, args ...any) error {
return HTTPError{
Expand Down
4 changes: 3 additions & 1 deletion internal/httputil/logging.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package httputil

import (
"fmt"
"net/http"

"github.com/block/sfptc/internal/logging"
)

func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := logging.FromContext(r.Context()).With("url", r.RequestURI)
// Propagate attributes tot the handlers.
logger := logging.FromContext(r.Context()).With("request", fmt.Sprintf("%s %s", r.Method, r.RequestURI))
r = r.WithContext(logging.ContextWithLogger(r.Context(), logger))
logger.Debug("Request received")
next.ServeHTTP(w, r)
Expand Down
88 changes: 20 additions & 68 deletions internal/strategy/github_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"maps"
"net/http"
"os"
"slices"

"github.com/alecthomas/errors"

"github.com/block/sfptc/internal/cache"
"github.com/block/sfptc/internal/httputil"
"github.com/block/sfptc/internal/logging"
"github.com/block/sfptc/internal/strategy/handler"
)

func init() {
Expand All @@ -31,90 +29,44 @@ type GitHubReleasesConfig struct {
type GitHubReleases struct {
config GitHubReleasesConfig
cache cache.Cache
client *http.Client
}

// NewGitHubReleases creates a [Strategy] that fetches private (and public) release binaries from GitHub.
func NewGitHubReleases(ctx context.Context, config GitHubReleasesConfig, cache cache.Cache, mux Mux) (*GitHubReleases, error) {
s := &GitHubReleases{
config: config,
cache: cache,
client: http.DefaultClient,
}
logger := logging.FromContext(ctx)
if config.Token == "" {
logger.WarnContext(ctx, "No token configured for github-releases strategy")
}
// eg. https://github.com/alecthomas/chroma/releases/download/v2.21.1/chroma-2.21.1-darwin-amd64.tar.gz
mux.Handle("GET /github.com/{org}/{repo}/releases/download/{release}/{file}", http.HandlerFunc(s.fetch))
h := handler.New(s.client, cache).
CacheKey(func(r *http.Request) string {
org := r.PathValue("org")
repo := r.PathValue("repo")
release := r.PathValue("release")
file := r.PathValue("file")
return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", org, repo, release, file)
}).
Transform(func(r *http.Request) (*http.Request, error) {
org := r.PathValue("org")
repo := r.PathValue("repo")
release := r.PathValue("release")
file := r.PathValue("file")
return s.downloadRelease(r.Context(), org, repo, release, file)
})
mux.Handle("GET /github.com/{org}/{repo}/releases/download/{release}/{file}", h)
return s, nil
}

var _ Strategy = (*GitHubReleases)(nil)

func (g *GitHubReleases) String() string { return "github-releases" }

func (g *GitHubReleases) fetch(w http.ResponseWriter, r *http.Request) {
org := r.PathValue("org")
repo := r.PathValue("repo")
release := r.PathValue("release")
file := r.PathValue("file")
ghURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", org, repo, release, file)

logger := logging.FromContext(r.Context()).With("upstream", ghURL)

key := cache.NewKey(ghURL)

logger.Debug("Fetching GitHub release")

// Check if the key exists in the cache
cr, headers, err := g.cache.Open(r.Context(), key)
if err == nil {
logger.Debug("Cache hit")
// Cache hit - stream directly from cache
defer cr.Close()
maps.Copy(w.Header(), headers)
if _, err := io.Copy(w, cr); err != nil {
httputil.ErrorResponse(w, r, http.StatusInternalServerError, "Failed to stream from cache", "error", err.Error())
return
}
return
}
if !errors.Is(err, os.ErrNotExist) {
httputil.ErrorResponse(w, r, http.StatusInternalServerError, "Failed to open cache", "error", err.Error())
return
}

// Cache miss - fetch from GitHub and stream while caching
req, err := g.downloadRelease(r.Context(), org, repo, release, file)
if err != nil {
if herr, ok := errors.AsType[httputil.HTTPError](err); ok {
httputil.ErrorResponse(w, r, herr.StatusCode(), herr.Error(), "upstream", ghURL)
} else {
httputil.ErrorResponse(w, r, http.StatusInternalServerError, "Failed to create download request", "error", err.Error())
}
return
}

response, err := cache.FetchDirect(http.DefaultClient, req, g.cache, key)
if err != nil {
if herr, ok := errors.AsType[httputil.HTTPError](err); ok {
httputil.ErrorResponse(w, r, herr.StatusCode(), herr.Error())
} else {
httputil.ErrorResponse(w, r, http.StatusInternalServerError, err.Error())
}
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
httputil.ErrorResponse(w, r, response.StatusCode, response.Status)
return
}
maps.Copy(w.Header(), response.Header)
if _, err := io.Copy(w, response.Body); err != nil {
httputil.ErrorResponse(w, r, http.StatusInternalServerError, "Failed to stream response", "error", err.Error())
return
}
}

// newGitHubRequest creates a new HTTP request with GitHub API headers and authentication.
func (g *GitHubReleases) newGitHubRequest(ctx context.Context, url, accept string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Expand Down Expand Up @@ -160,7 +112,7 @@ func (g *GitHubReleases) downloadRelease(ctx context.Context, org, repo, release
return nil, httputil.Errorf(http.StatusInternalServerError, "create API request")
}

resp, err := http.DefaultClient.Do(req)
resp, err := g.client.Do(req)
if err != nil {
return nil, httputil.Errorf(http.StatusBadGateway, "fetch release info failed: %w", err)
}
Expand Down
Loading