serve.go

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

Source Files