chore: refactor site handler to take cache dir (#21918)

relates to: https://github.com/coder/internal/issues/1300

Refactors the options to the site handler to take the cache directory, rather than expecting the caller to call `ExtractOrReadBinFS` and pass the results.

This is important in this stack because we need direct access to the cache directory for compressed file caching.
This commit is contained in:
Spike Curtis
2026-02-06 10:56:48 +04:00
committed by GitHub
parent 110dcbbb54
commit 6b1adb8b12
4 changed files with 46 additions and 42 deletions
+5 -7
View File
@@ -462,10 +462,6 @@ func New(options *Options) *API {
if siteCacheDir != "" { if siteCacheDir != "" {
siteCacheDir = filepath.Join(siteCacheDir, "site") siteCacheDir = filepath.Join(siteCacheDir, "site")
} }
binFS, binHashes, err := site.ExtractOrReadBinFS(siteCacheDir, site.FS())
if err != nil {
panic(xerrors.Errorf("read site bin failed: %w", err))
}
metricsCache := metricscache.New( metricsCache := metricscache.New(
options.Database, options.Database,
@@ -658,9 +654,8 @@ func New(options *Options) *API {
WebPushPublicKey: api.WebpushDispatcher.PublicKey(), WebPushPublicKey: api.WebpushDispatcher.PublicKey(),
Telemetry: api.Telemetry.Enabled(), Telemetry: api.Telemetry.Enabled(),
} }
api.SiteHandler = site.New(&site.Options{ api.SiteHandler, err = site.New(&site.Options{
BinFS: binFS, CacheDir: siteCacheDir,
BinHashes: binHashes,
Database: options.Database, Database: options.Database,
SiteFS: site.FS(), SiteFS: site.FS(),
OAuth2Configs: oauthConfigs, OAuth2Configs: oauthConfigs,
@@ -672,6 +667,9 @@ func New(options *Options) *API {
Logger: options.Logger.Named("site"), Logger: options.Logger.Named("site"),
HideAITasks: options.DeploymentValues.HideAITasks.Value(), HideAITasks: options.DeploymentValues.HideAITasks.Value(),
}) })
if err != nil {
options.Logger.Fatal(ctx, "failed to initialize site handler", slog.Error(err))
}
api.SiteHandler.Experiments.Store(&experiments) api.SiteHandler.Experiments.Store(&experiments)
if options.UpdateCheckOptions != nil { if options.UpdateCheckOptions != nil {
+8 -4
View File
@@ -89,11 +89,15 @@ func (h *binHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
http.FileServer(h.binFS).ServeHTTP(rw, r) http.FileServer(h.binFS).ServeHTTP(rw, r)
} }
func newBinHandler(options *Options) *binHandler { func newBinHandler(options *Options) (*binHandler, error) {
return &binHandler{ binFS, binHashes, err := ExtractOrReadBinFS(options.CacheDir, options.SiteFS)
binFS: options.BinFS, if err != nil {
metadataCache: newBinMetadataCache(options.BinFS, options.BinHashes), return nil, xerrors.Errorf("extract or read bin filesystem: %w", err)
} }
return &binHandler{
binFS: binFS,
metadataCache: newBinMetadataCache(binFS, binHashes),
}, nil
} }
// ExtractOrReadBinFS checks the provided fs for compressed coder binaries and // ExtractOrReadBinFS checks the provided fs for compressed coder binaries and
+11 -7
View File
@@ -68,8 +68,7 @@ func init() {
} }
type Options struct { type Options struct {
BinFS http.FileSystem CacheDir string
BinHashes map[string]string
Database database.Store Database database.Store
SiteFS fs.FS SiteFS fs.FS
OAuth2Configs *httpmw.OAuth2Configs OAuth2Configs *httpmw.OAuth2Configs
@@ -82,7 +81,7 @@ type Options struct {
HideAITasks bool HideAITasks bool
} }
func New(opts *Options) *Handler { func New(opts *Options) (*Handler, error) {
if opts.AppearanceFetcher == nil { if opts.AppearanceFetcher == nil {
daf := atomic.Pointer[appearance.Fetcher]{} daf := atomic.Pointer[appearance.Fetcher]{}
f := appearance.NewDefaultFetcher(opts.DocsURL) f := appearance.NewDefaultFetcher(opts.DocsURL)
@@ -100,11 +99,16 @@ func New(opts *Options) *Handler {
var err error var err error
handler.htmlTemplates, err = findAndParseHTMLFiles(opts.SiteFS) handler.htmlTemplates, err = findAndParseHTMLFiles(opts.SiteFS)
if err != nil { if err != nil {
panic(fmt.Sprintf("Failed to parse html files: %v", err)) return nil, xerrors.Errorf("failed to parse html files: %w", err)
}
binHand, err := newBinHandler(opts)
if err != nil {
return nil, xerrors.Errorf("create bin handler: %w", err)
} }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/bin/", newBinHandler(opts)) mux.Handle("/bin/", binHand)
mux.Handle("/", http.FileServer( mux.Handle("/", http.FileServer(
http.FS( http.FS(
// OnlyFiles is a wrapper around the file system that prevents directory // OnlyFiles is a wrapper around the file system that prevents directory
@@ -116,7 +120,7 @@ func New(opts *Options) *Handler {
) )
buildInfoResponse, err := json.Marshal(opts.BuildInfo) buildInfoResponse, err := json.Marshal(opts.BuildInfo)
if err != nil { if err != nil {
panic("failed to marshal build info: " + err.Error()) return nil, xerrors.Errorf("failed to marshal build info: %w", err)
} }
handler.buildInfoJSON = html.EscapeString(string(buildInfoResponse)) handler.buildInfoJSON = html.EscapeString(string(buildInfoResponse))
handler.handler = mux.ServeHTTP handler.handler = mux.ServeHTTP
@@ -126,7 +130,7 @@ func New(opts *Options) *Handler {
opts.Logger.Warn(context.Background(), "could not parse install.sh, it will be unavailable", slog.Error(err)) opts.Logger.Warn(context.Background(), "could not parse install.sh, it will be unavailable", slog.Error(err))
} }
return handler return handler, nil
} }
type Handler struct { type Handler struct {
+22 -24
View File
@@ -23,6 +23,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -44,14 +45,13 @@ func TestInjection(t *testing.T) {
Data: []byte("{{ .User }}"), Data: []byte("{{ .User }}"),
}, },
} }
binFs := http.FS(fstest.MapFS{})
db, _ := dbtestutil.NewDB(t) db, _ := dbtestutil.NewDB(t)
handler := site.New(&site.Options{ handler, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(), Telemetry: telemetry.NewNoop(),
BinFS: binFs,
Database: db, Database: db,
SiteFS: siteFS, SiteFS: siteFS,
}) })
require.NoError(t, err)
user := dbgen.User(t, db, database.User{}) user := dbgen.User(t, db, database.User{})
_, token := dbgen.APIKey(t, db, database.APIKey{ _, token := dbgen.APIKey(t, db, database.APIKey{
@@ -66,7 +66,7 @@ func TestInjection(t *testing.T) {
handler.ServeHTTP(rw, r) handler.ServeHTTP(rw, r)
require.Equal(t, http.StatusOK, rw.Code) require.Equal(t, http.StatusOK, rw.Code)
var got codersdk.User var got codersdk.User
err := json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &got) err = json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &got)
require.NoError(t, err) require.NoError(t, err)
// This will update as part of the request! // This will update as part of the request!
@@ -101,15 +101,13 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) {
OAuthExpiry: dbtime.Now().Add(-time.Second), OAuthExpiry: dbtime.Now().Add(-time.Second),
}) })
binFs := http.FS(fstest.MapFS{})
siteFS := fstest.MapFS{ siteFS := fstest.MapFS{
"index.html": &fstest.MapFile{ "index.html": &fstest.MapFile{
Data: []byte("<html>{{ .User }}</html>"), Data: []byte("<html>{{ .User }}</html>"),
}, },
} }
handler := site.New(&site.Options{ handler, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(), Telemetry: telemetry.NewNoop(),
BinFS: binFs,
Database: db, Database: db,
SiteFS: siteFS, SiteFS: siteFS,
@@ -119,6 +117,7 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) {
OIDC: nil, OIDC: nil,
}, },
}) })
require.NoError(t, err)
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
r.Header.Set(codersdk.SessionTokenHeader, token) r.Header.Set(codersdk.SessionTokenHeader, token)
@@ -153,15 +152,15 @@ func TestCaching(t *testing.T) {
Data: []byte("folderFile"), Data: []byte("folderFile"),
}, },
} }
binFS := http.FS(fstest.MapFS{})
db, _ := dbtestutil.NewDB(t) db, _ := dbtestutil.NewDB(t)
srv := httptest.NewServer(site.New(&site.Options{ s, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(), Telemetry: telemetry.NewNoop(),
BinFS: binFS,
SiteFS: rootFS, SiteFS: rootFS,
Database: db, Database: db,
})) })
require.NoError(t, err)
srv := httptest.NewServer(s)
defer srv.Close() defer srv.Close()
// Create a context // Create a context
@@ -222,15 +221,15 @@ func TestServingFiles(t *testing.T) {
Data: []byte("install-sh-bytes"), Data: []byte("install-sh-bytes"),
}, },
} }
binFS := http.FS(fstest.MapFS{})
db, _ := dbtestutil.NewDB(t) db, _ := dbtestutil.NewDB(t)
srv := httptest.NewServer(site.New(&site.Options{ handler, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(), Telemetry: telemetry.NewNoop(),
BinFS: binFS,
SiteFS: rootFS, SiteFS: rootFS,
Database: db, Database: db,
})) })
require.NoError(t, err)
srv := httptest.NewServer(handler)
defer srv.Close() defer srv.Close()
client := &http.Client{} client := &http.Client{}
@@ -506,21 +505,20 @@ func TestServingBin(t *testing.T) {
t.Parallel() t.Parallel()
dest := t.TempDir() dest := t.TempDir()
binFS, binHashes, err := site.ExtractOrReadBinFS(dest, tt.fs) testFS := maps.Clone(rootFS)
maps.Copy(testFS, tt.fs)
handler, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(),
SiteFS: testFS,
CacheDir: dest,
})
if !tt.wantErr && err != nil { if !tt.wantErr && err != nil {
require.NoError(t, err, "extract or read failed") require.NoError(t, err, "extract or read failed")
} else if tt.wantErr { } else if tt.wantErr {
require.Error(t, err, "extraction or read did not fail") require.Error(t, err, "extraction or read did not fail")
} }
site := site.New(&site.Options{
Telemetry: telemetry.NewNoop(),
BinFS: binFS,
BinHashes: binHashes,
SiteFS: rootFS,
})
compressor := middleware.NewCompressor(1, "text/*", "application/*") compressor := middleware.NewCompressor(1, "text/*", "application/*")
srv := httptest.NewServer(compressor.Handler(site)) srv := httptest.NewServer(compressor.Handler(handler))
defer srv.Close() defer srv.Close()
client := &http.Client{} client := &http.Client{}