Skip to content

Commit 58fbad3

Browse files
committed
claude-based storage/service interface division, missing preconditions
1 parent c0f593d commit 58fbad3

File tree

7 files changed

+452
-501
lines changed

7 files changed

+452
-501
lines changed

cmd/catalogd/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,11 @@ func run(ctx context.Context) error {
365365
return err
366366
}
367367

368-
localStorage = &storage.LocalDirV1{
369-
RootDir: storeDir,
370-
RootURL: baseStorageURL,
371-
EnableMetasHandler: features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler),
372-
}
368+
localStorage = storage.NewLocalDirV1(
369+
storeDir,
370+
baseStorageURL,
371+
features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler),
372+
)
373373

374374
// Config for the catalogd web server
375375
catalogServerConfig := serverutil.CatalogServerConfig{
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
13+
"k8s.io/apimachinery/pkg/util/sets"
14+
15+
"github.com/operator-framework/operator-controller/internal/catalogd/service"
16+
)
17+
18+
var errInvalidParams = errors.New("invalid parameters")
19+
20+
const timeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
21+
22+
// CatalogHandlers handles HTTP requests for catalog content
23+
type CatalogHandlers struct {
24+
store CatalogStore
25+
graphqlSvc service.GraphQLService
26+
rootURL *url.URL
27+
enableMetas bool
28+
}
29+
30+
// Index provides methods for looking up catalog content by schema/package/name
31+
type Index interface {
32+
Get(catalogFile io.ReaderAt, schema, pkg, name string) io.Reader
33+
}
34+
35+
// CatalogStore defines the storage interface needed by handlers
36+
type CatalogStore interface {
37+
// GetCatalogData returns the catalog file and its metadata
38+
GetCatalogData(catalog string) (*os.File, os.FileInfo, error)
39+
40+
// GetCatalogFS returns a filesystem interface for the catalog
41+
GetCatalogFS(catalog string) (fs.FS, error)
42+
43+
// GetIndex returns the index for a catalog (if metas handler is enabled)
44+
GetIndex(catalog string) (Index, error)
45+
}
46+
47+
// NewCatalogHandlers creates a new HTTP handlers instance
48+
func NewCatalogHandlers(store CatalogStore, graphqlSvc service.GraphQLService, rootURL *url.URL, enableMetas bool) *CatalogHandlers {
49+
return &CatalogHandlers{
50+
store: store,
51+
graphqlSvc: graphqlSvc,
52+
rootURL: rootURL,
53+
enableMetas: enableMetas,
54+
}
55+
}
56+
57+
// Handler returns an HTTP handler with all routes configured
58+
func (h *CatalogHandlers) Handler() http.Handler {
59+
mux := http.NewServeMux()
60+
61+
mux.HandleFunc(h.rootURL.JoinPath("{catalog}", "api", "v1", "all").Path, h.handleV1All)
62+
if h.enableMetas {
63+
mux.HandleFunc(h.rootURL.JoinPath("{catalog}", "api", "v1", "metas").Path, h.handleV1Metas)
64+
}
65+
mux.HandleFunc(h.rootURL.JoinPath("{catalog}", "api", "v1", "graphql").Path, h.handleV1GraphQL)
66+
67+
return allowedMethodsHandler(mux, http.MethodGet, http.MethodHead, http.MethodPost)
68+
}
69+
70+
// handleV1All serves the complete catalog content
71+
func (h *CatalogHandlers) handleV1All(w http.ResponseWriter, r *http.Request) {
72+
catalog := r.PathValue("catalog")
73+
catalogFile, catalogStat, err := h.store.GetCatalogData(catalog)
74+
if err != nil {
75+
httpError(w, err)
76+
return
77+
}
78+
defer catalogFile.Close()
79+
80+
w.Header().Add("Content-Type", "application/jsonl")
81+
http.ServeContent(w, r, "", catalogStat.ModTime(), catalogFile)
82+
}
83+
84+
// handleV1Metas serves filtered catalog content based on query parameters
85+
func (h *CatalogHandlers) handleV1Metas(w http.ResponseWriter, r *http.Request) {
86+
// Check for unexpected query parameters
87+
expectedParams := map[string]bool{
88+
"schema": true,
89+
"package": true,
90+
"name": true,
91+
}
92+
93+
for param := range r.URL.Query() {
94+
if !expectedParams[param] {
95+
httpError(w, errInvalidParams)
96+
return
97+
}
98+
}
99+
100+
catalog := r.PathValue("catalog")
101+
catalogFile, catalogStat, err := h.store.GetCatalogData(catalog)
102+
if err != nil {
103+
httpError(w, err)
104+
return
105+
}
106+
defer catalogFile.Close()
107+
108+
w.Header().Set("Last-Modified", catalogStat.ModTime().UTC().Format(timeFormat))
109+
done := checkPreconditions(w, r, catalogStat.ModTime())
110+
if done {
111+
return
112+
}
113+
114+
schema := r.URL.Query().Get("schema")
115+
pkg := r.URL.Query().Get("package")
116+
name := r.URL.Query().Get("name")
117+
118+
if schema == "" && pkg == "" && name == "" {
119+
// If no parameters are provided, return the entire catalog
120+
serveJSONLines(w, r, catalogFile)
121+
return
122+
}
123+
124+
idx, err := h.store.GetIndex(catalog)
125+
if err != nil {
126+
httpError(w, err)
127+
return
128+
}
129+
indexReader := idx.Get(catalogFile, schema, pkg, name)
130+
serveJSONLines(w, r, indexReader)
131+
}
132+
133+
// handleV1GraphQL handles GraphQL queries
134+
func (h *CatalogHandlers) handleV1GraphQL(w http.ResponseWriter, r *http.Request) {
135+
if r.Method != http.MethodPost {
136+
http.Error(w, "Only POST is allowed", http.StatusMethodNotAllowed)
137+
return
138+
}
139+
140+
catalog := r.PathValue("catalog")
141+
142+
// Parse GraphQL query from request body
143+
var params struct {
144+
Query string `json:"query"`
145+
}
146+
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
147+
http.Error(w, "Invalid request body", http.StatusBadRequest)
148+
return
149+
}
150+
151+
// Get catalog filesystem
152+
catalogFS, err := h.store.GetCatalogFS(catalog)
153+
if err != nil {
154+
httpError(w, err)
155+
return
156+
}
157+
158+
// Execute GraphQL query through the service
159+
result, err := h.graphqlSvc.ExecuteQuery(catalog, catalogFS, params.Query)
160+
if err != nil {
161+
httpError(w, err)
162+
return
163+
}
164+
165+
w.Header().Set("Content-Type", "application/json")
166+
if err := json.NewEncoder(w).Encode(result); err != nil {
167+
httpError(w, err)
168+
return
169+
}
170+
}
171+
172+
// httpError writes an HTTP error response based on the error type
173+
func httpError(w http.ResponseWriter, err error) {
174+
var code int
175+
switch {
176+
case errors.Is(err, fs.ErrNotExist):
177+
code = http.StatusNotFound
178+
case errors.Is(err, fs.ErrPermission):
179+
code = http.StatusForbidden
180+
case errors.Is(err, errInvalidParams):
181+
code = http.StatusBadRequest
182+
default:
183+
code = http.StatusInternalServerError
184+
}
185+
// Log the actual error for debugging
186+
fmt.Printf("HTTP Error %d: %v\n", code, err)
187+
http.Error(w, fmt.Sprintf("%d %s", code, http.StatusText(code)), code)
188+
}
189+
190+
// serveJSONLines writes JSON lines content to the response
191+
func serveJSONLines(w http.ResponseWriter, r *http.Request, rs io.Reader) {
192+
w.Header().Add("Content-Type", "application/jsonl")
193+
// Copy the content of the reader to the response writer only if it's a GET request
194+
if r.Method == http.MethodHead {
195+
return
196+
}
197+
_, err := io.Copy(w, rs)
198+
if err != nil {
199+
httpError(w, err)
200+
return
201+
}
202+
}
203+
204+
// allowedMethodsHandler wraps a handler to only allow specific HTTP methods
205+
func allowedMethodsHandler(next http.Handler, allowedMethods ...string) http.Handler {
206+
allowedMethodSet := sets.New[string](allowedMethods...)
207+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
208+
// Allow POST requests only for GraphQL endpoint
209+
if r.URL.Path != "" && len(r.URL.Path) >= 7 && r.URL.Path[len(r.URL.Path)-7:] != "graphql" && r.Method == http.MethodPost {
210+
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
211+
return
212+
}
213+
if !allowedMethodSet.Has(r.Method) {
214+
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
215+
return
216+
}
217+
next.ServeHTTP(w, r)
218+
})
219+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package server
2+
3+
import (
4+
"net/http"
5+
"time"
6+
)
7+
8+
// checkPreconditions checks HTTP preconditions (If-Modified-Since, If-Unmodified-Since)
9+
// Returns true if the request has already been handled (e.g., 304 Not Modified response sent)
10+
func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool) {
11+
// Check If-Modified-Since
12+
if r.Method == http.MethodGet || r.Method == http.MethodHead {
13+
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
14+
// The Date-Modified header truncates sub-second precision, so
15+
// use ModTime < t+1s instead of ModTime <= t to check for unmodified.
16+
if modtime.Before(t.Add(time.Second)) {
17+
w.WriteHeader(http.StatusNotModified)
18+
return true
19+
}
20+
}
21+
}
22+
23+
// Check If-Unmodified-Since
24+
if r.Method != http.MethodGet && r.Method != http.MethodHead {
25+
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil {
26+
// The Date-Modified header truncates sub-second precision, so
27+
// use ModTime >= t+1s instead of ModTime > t to check for modified.
28+
if modtime.After(t.Add(-time.Second)) {
29+
w.WriteHeader(http.StatusPreconditionFailed)
30+
return true
31+
}
32+
}
33+
}
34+
35+
return false
36+
}

0 commit comments

Comments
 (0)