serve.go

v1.0.0
Doc Versions Source
1
package main
2
3
import (
4
	"context"
5
	"fmt"
6
	"log"
7
	"net/http"
8
	"time"
9
10
	"github.com/prometheus/client_golang/prometheus/promhttp"
11
	"github.com/spf13/cobra"
12
	"golang.org/x/mod/sumdb"
13
14
	"example.com/curator/internal/admin"
15
	"example.com/curator/internal/config"
16
	"example.com/curator/internal/dochtml"
17
	"example.com/curator/internal/git"
18
	"example.com/curator/internal/metrics"
19
	"example.com/curator/internal/server"
20
	"example.com/curator/internal/storage"
21
	"example.com/curator/internal/store"
22
	isumdb "example.com/curator/internal/sumdb"
23
)
24
25
func serveCmd() *cobra.Command {
26
	var configPath string
27
28
	cmd := &cobra.Command{
29
		Use:   "serve",
30
		Short: "Start the curator HTTP server",
31
		RunE: func(cmd *cobra.Command, args []string) error {
32
			return runServe(configPath)
33
		},
34
	}
35
36
	cmd.Flags().StringVarP(&configPath, "config", "c", "", "path to config file (optional, env vars always apply)")
37
38
	return cmd
39
}
40
41
func runServe(configPath string) error {
42
	cfg, err := config.Load(configPath)
43
	if err != nil {
44
		return err
45
	}
46
47
	log.Printf("configuration:\n%s", cfg.Dump())
48
49
	// Open module store.
50
	moduleStore, err := store.Open(cfg.Database.Type, cfg.Database.DSN)
51
	if err != nil {
52
		return fmt.Errorf("open store: %w", err)
53
	}
54
	defer moduleStore.Close()
55
56
	docRenderer, err := dochtml.NewRenderer()
57
	if err != nil {
58
		return fmt.Errorf("init doc renderer: %w", err)
59
	}
60
61
	srv := &server.Server{
62
		Cfg:         cfg,
63
		Resolver:    moduleStore,
64
		Git:         git.NewCache(cfg.Cache),
65
		HTTPClient:  &http.Client{Timeout: 30 * time.Second},
66
		DocRenderer: docRenderer,
67
	}
68
69
	// Load auth tokens if configured.
70
	if cfg.AuthTokens != "" {
71
		tokens, err := server.ParseAuthTokens(cfg.AuthTokens)
72
		if err != nil {
73
			return fmt.Errorf("load auth tokens: %w", err)
74
		}
75
76
		srv.AuthTokens = tokens
77
		log.Printf("loaded %d auth tokens", len(tokens))
78
	}
79
80
	// Initialize storage if configured.
81
	if cfg.Storage != nil {
82
		st, err := initStorage(context.Background(), cfg.Storage)
83
		if err != nil {
84
			return fmt.Errorf("init storage: %w", err)
85
		}
86
87
		srv.Store = st
88
		storageType := cfg.Storage.Type
89
		if storageType == "" {
90
			storageType = "s3"
91
		}
92
		log.Printf("storage enabled: type=%s", storageType)
93
	}
94
95
	// Initialize sumdb if configured.
96
	var sumdbOps *isumdb.Ops
97
98
	if cfg.Sumdb != nil && cfg.Sumdb.Enabled {
99
		signer, err := config.ResolveValue(cfg.Sumdb.Key)
100
		if err != nil {
101
			return fmt.Errorf("read sumdb key: %w", err)
102
		}
103
		sumdbOps = isumdb.NewOps(cfg.Host, moduleStore, srv.Git, moduleStore, signer)
104
		log.Printf("sumdb enabled with %d records", sumdbOps.RecordCount())
105
	}
106
107
	// Metrics.
108
	metrics.Register()
109
110
	ctx, cancel := context.WithCancel(context.Background())
111
	defer cancel()
112
113
	var bucketStats metrics.BucketStatsFunc
114
	if srv.Store != nil {
115
		if sp, ok := srv.Store.(storage.StatsProvider); ok {
116
			bucketStats = sp.BucketStats
117
		}
118
	}
119
120
	var recordCount metrics.RecordCountFunc
121
	if sumdbOps != nil {
122
		recordCount = sumdbOps.RecordCount
123
	}
124
125
	moduleCount := func() int { return moduleStore.ModuleCount() }
126
	metrics.StartCollector(ctx, cfg.Cache, moduleCount, bucketStats, recordCount, 30*time.Second)
127
128
	// Instrumentation server (metrics + health) on a separate port.
129
	if inst := cfg.Instrumentation; inst != nil && inst.Listen != "" {
130
		instrMux := http.NewServeMux()
131
132
		metricsPath := inst.MetricsPath
133
		if metricsPath == "" {
134
			metricsPath = "/metrics"
135
		}
136
		instrMux.Handle(metricsPath, promhttp.Handler())
137
138
		healthPath := inst.HealthPath
139
		if healthPath == "" {
140
			healthPath = "/healthz"
141
		}
142
		instrMux.HandleFunc(healthPath, healthHandler)
143
144
		go func() {
145
			log.Printf("instrumentation serving on %s (metrics: %s, health: %s)",
146
				inst.Listen, metricsPath, healthPath)
147
			if err := http.ListenAndServe(inst.Listen, instrMux); err != nil {
148
				log.Fatalf("instrumentation server: %v", err)
149
			}
150
		}()
151
	}
152
153
	// Main HTTP mux.
154
	mux := http.NewServeMux()
155
	mux.Handle("/-/static/", http.StripPrefix("/-/static/", http.FileServer(dochtml.StaticFS())))
156
157
	// Admin UI and API.
158
	adminHandler, err := admin.New(moduleStore)
159
	if err != nil {
160
		return fmt.Errorf("init admin: %w", err)
161
	}
162
163
	apiHandler := admin.NewAPI(moduleStore)
164
165
	adminToken := cfg.AdminToken
166
	if adminToken != "" {
167
		parsed, err := server.ParseAdminToken(adminToken)
168
		if err != nil {
169
			return fmt.Errorf("parse admin token: %w", err)
170
		}
171
		adminToken = parsed
172
	}
173
	mux.Handle("/-/admin/", admin.AuthMiddleware(adminToken, adminHandler))
174
	mux.Handle("/-/api/", admin.AuthMiddleware(adminToken, apiHandler))
175
176
	if sumdbOps != nil {
177
		sumdbSrv := sumdb.NewServer(sumdbOps)
178
		prefix := "/sumdb/" + cfg.Host
179
180
		var upstream string
181
		if cfg.Sumdb.Upstream != "" {
182
			upstream = cfg.Sumdb.Upstream
183
			log.Printf("sumdb upstream proxy: %s", upstream)
184
		}
185
186
		handler := srv.SumdbHandler(
187
			http.StripPrefix(prefix, sumdbSrv),
188
			upstream, prefix,
189
		)
190
		mux.Handle(prefix+"/", srv.SumdbAuthMiddleware(handler))
191
		log.Printf("sumdb serving at %s/", prefix)
192
	}
193
194
	mux.Handle("/", srv)
195
196
	log.Printf("curator serving on %s (host: %s, db: %s/%s, modules: %d)",
197
		cfg.Listen, cfg.Host, cfg.Database.Type, cfg.Database.DSN, moduleStore.ModuleCount())
198
199
	return http.ListenAndServe(cfg.Listen, mux)
200
}
201
202
func initStorage(ctx context.Context, cfg *config.StorageConfig) (storage.Storage, error) {
203
	switch cfg.Type {
204
	case "s3", "":
205
		return storage.NewS3Storage(ctx, cfg.S3())
206
	case "disk":
207
		return storage.NewDiskStorage(cfg.Root)
208
	case "memory":
209
		return storage.NewMemoryStorage(cfg.MaxBytes), nil
210
	default:
211
		return nil, fmt.Errorf("unknown storage type: %q", cfg.Type)
212
	}
213
}
214
215
func healthHandler(w http.ResponseWriter, r *http.Request) {
216
	w.Header().Set("Content-Type", "application/json")
217
	w.Write([]byte(`{"status":"ok"}` + "\n"))
218
}
219

Source Files