Skip to content

Commit 9e65b3e

Browse files
committed
integration with existing http service endpoint
Signed-off-by: grokspawn <[email protected]>
1 parent 0879aa6 commit 9e65b3e

File tree

3 files changed

+225
-37
lines changed

3 files changed

+225
-37
lines changed

internal/catalogd/graphql/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# GraphQL Integration
2+
3+
This package provides dynamic GraphQL schema generation for operator catalog data, integrated into the catalogd storage server.
4+
5+
## Usage
6+
7+
The GraphQL endpoint is now available as part of the catalogd storage server at:
8+
9+
```
10+
{catalog}/api/v1/graphql
11+
```
12+
13+
Where `{catalog}` is replaced by the actual catalog name at runtime.
14+
15+
## Example Usage
16+
17+
### Making a GraphQL Request
18+
19+
```bash
20+
curl -X POST http://localhost:8080/my-catalog/api/v1/graphql \
21+
-H "Content-Type: application/json" \
22+
-d '{
23+
"query": "{ summary { totalSchemas schemas { name totalObjects totalFields } } }"
24+
}'
25+
```
26+
27+
### Sample Queries
28+
29+
#### Get catalog summary:
30+
```graphql
31+
{
32+
summary {
33+
totalSchemas
34+
schemas {
35+
name
36+
totalObjects
37+
totalFields
38+
}
39+
}
40+
}
41+
```
42+
43+
#### Get bundles with pagination:
44+
```graphql
45+
{
46+
bundles(limit: 5, offset: 0) {
47+
name
48+
package
49+
version
50+
}
51+
}
52+
```
53+
54+
#### Get packages:
55+
```graphql
56+
{
57+
packages(limit: 10) {
58+
name
59+
description
60+
}
61+
}
62+
```
63+
64+
#### Get bundle properties (union types):
65+
```graphql
66+
{
67+
bundles(limit: 5) {
68+
name
69+
properties {
70+
type
71+
value {
72+
... on PropertyValueFeaturesOperatorsOpenshiftIo {
73+
disconnected
74+
cnf
75+
cni
76+
csi
77+
fips
78+
}
79+
}
80+
}
81+
}
82+
}
83+
```
84+
85+
## Features
86+
87+
- **Dynamic Schema Generation**: Automatically discovers schema structure from catalog metadata
88+
- **Union Types**: Supports complex bundle properties with variable structures
89+
- **Pagination**: Built-in limit/offset pagination for all queries
90+
- **Field Name Sanitization**: Converts JSON field names to valid GraphQL identifiers
91+
- **Catalog-Specific**: Each catalog gets its own dynamically generated schema
92+
93+
## Integration
94+
95+
The GraphQL functionality is integrated into the `LocalDirV1` storage handler in `internal/catalogd/storage/localdir.go`:
96+
97+
- `handleV1GraphQL()`: Handles POST requests to the GraphQL endpoint
98+
- `createCatalogFS()`: Creates filesystem interface for catalog data
99+
- `buildCatalogGraphQLSchema()`: Builds dynamic GraphQL schema for specific catalogs
100+
101+
## Technical Details
102+
103+
- Uses `declcfg.WalkMetasFS` to discover schema structure
104+
- Generates GraphQL object types dynamically from discovered fields
105+
- Creates union types for bundle properties with variable structures
106+
- Supports all standard GraphQL features including introspection

internal/catalogd/graphql/graphql.go

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"fmt"
77
"io/fs"
8-
"net/http"
98
"reflect"
109
"regexp"
1110
"strings"
@@ -14,8 +13,6 @@ import (
1413
"github.com/operator-framework/operator-registry/alpha/declcfg"
1514
)
1615

17-
var schema graphql.Schema
18-
1916
// FieldInfo represents discovered field information
2017
type FieldInfo struct {
2118
Name string
@@ -565,7 +562,7 @@ func BuildDynamicGraphQLSchema(catalogSchema *CatalogSchema, metasBySchema map[s
565562
}
566563

567564
// LoadAndSummarizeCatalogDynamic loads FBC using WalkMetasReader and builds dynamic GraphQL schema
568-
func LoadAndSummarizeCatalogDynamic(catalogFS fs.FS) error {
565+
func LoadAndSummarizeCatalogDynamic(catalogFS fs.FS) (*DynamicSchema, error) {
569566
var metas []*declcfg.Meta
570567

571568
// Collect all metas from the filesystem
@@ -579,13 +576,13 @@ func LoadAndSummarizeCatalogDynamic(catalogFS fs.FS) error {
579576
return nil
580577
})
581578
if err != nil {
582-
return fmt.Errorf("error walking catalog metas: %w", err)
579+
return nil, fmt.Errorf("error walking catalog metas: %w", err)
583580
}
584581

585582
// Discover schema from collected metas
586583
catalogSchema, err := DiscoverSchemaFromMetas(metas)
587584
if err != nil {
588-
return fmt.Errorf("error discovering schema: %w", err)
585+
return nil, fmt.Errorf("error discovering schema: %w", err)
589586
}
590587

591588
// Organize metas by schema for resolvers
@@ -599,11 +596,15 @@ func LoadAndSummarizeCatalogDynamic(catalogFS fs.FS) error {
599596
// Build dynamic GraphQL schema
600597
dynamicSchema, err := BuildDynamicGraphQLSchema(catalogSchema, metasBySchema)
601598
if err != nil {
602-
return fmt.Errorf("error building GraphQL schema: %w", err)
599+
return nil, fmt.Errorf("error building GraphQL schema: %w", err)
603600
}
604601

605-
// Set the global schema for serving
606-
schema = dynamicSchema.Schema
602+
return dynamicSchema, nil
603+
}
604+
605+
// PrintCatalogSummary prints a comprehensive summary of the discovered schema
606+
func PrintCatalogSummary(dynamicSchema *DynamicSchema) {
607+
catalogSchema := dynamicSchema.CatalogSchema
607608

608609
// Print comprehensive summary
609610
fmt.Printf("Dynamic GraphQL schema generation complete.\n")
@@ -661,32 +662,4 @@ func LoadAndSummarizeCatalogDynamic(catalogFS fs.FS) error {
661662
fmt.Printf(" packages(limit: 5) { name }\n")
662663
}
663664
fmt.Printf("}\n")
664-
665-
return nil
666-
}
667-
668-
// ServeGraphQL starts an HTTPS server with the /graphql endpoint.
669-
func ServeGraphQL(listenAddr, certPath, keyPath string) error {
670-
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
671-
if r.Method != http.MethodPost {
672-
http.Error(w, "Only POST is allowed", http.StatusMethodNotAllowed)
673-
return
674-
}
675-
var params struct {
676-
Query string `json:"query"`
677-
}
678-
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
679-
http.Error(w, "Invalid request body", http.StatusBadRequest)
680-
return
681-
}
682-
result := graphql.Do(graphql.Params{
683-
Schema: schema,
684-
RequestString: params.Query,
685-
})
686-
w.Header().Set("Content-Type", "application/json")
687-
json.NewEncoder(w).Encode(result)
688-
})
689-
690-
fmt.Printf("Serving GraphQL endpoint at https://%s/graphql\n", listenAddr)
691-
return http.ListenAndServeTLS(listenAddr, certPath, keyPath, nil)
692665
}

internal/catalogd/storage/localdir.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"golang.org/x/sync/singleflight"
1818
"k8s.io/apimachinery/pkg/util/sets"
1919

20+
"github.com/graphql-go/graphql"
21+
gql "github.com/operator-framework/operator-controller/internal/catalogd/graphql"
2022
"github.com/operator-framework/operator-registry/alpha/declcfg"
2123
)
2224

@@ -192,9 +194,16 @@ func (s *LocalDirV1) StorageServerHandler() http.Handler {
192194
if s.EnableMetasHandler {
193195
mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "metas").Path, s.handleV1Metas)
194196
}
197+
mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "graphql").Path, s.handleV1GraphQL)
198+
195199
allowedMethodsHandler := func(next http.Handler, allowedMethods ...string) http.Handler {
196200
allowedMethodSet := sets.New[string](allowedMethods...)
197201
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
202+
// Allow POST requests for GraphQL endpoint
203+
if r.URL.Path != "" && r.URL.Path[len(r.URL.Path)-7:] == "graphql" && r.Method == http.MethodPost {
204+
next.ServeHTTP(w, r)
205+
return
206+
}
198207
if !allowedMethodSet.Has(r.Method) {
199208
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
200209
return
@@ -269,6 +278,59 @@ func (s *LocalDirV1) handleV1Metas(w http.ResponseWriter, r *http.Request) {
269278
serveJSONLines(w, r, indexReader)
270279
}
271280

281+
func (s *LocalDirV1) handleV1GraphQL(w http.ResponseWriter, r *http.Request) {
282+
s.m.RLock()
283+
defer s.m.RUnlock()
284+
285+
if r.Method != http.MethodPost {
286+
http.Error(w, "Only POST is allowed", http.StatusMethodNotAllowed)
287+
return
288+
}
289+
290+
catalog := r.PathValue("catalog")
291+
catalogFile, _, err := s.catalogData(catalog)
292+
if err != nil {
293+
httpError(w, err)
294+
return
295+
}
296+
defer catalogFile.Close()
297+
298+
// Parse GraphQL query from request body
299+
var params struct {
300+
Query string `json:"query"`
301+
}
302+
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
303+
http.Error(w, "Invalid request body", http.StatusBadRequest)
304+
return
305+
}
306+
307+
// Create catalog filesystem from the stored data
308+
catalogFS, err := s.createCatalogFS(catalog)
309+
if err != nil {
310+
httpError(w, err)
311+
return
312+
}
313+
314+
// Build dynamic GraphQL schema for this catalog
315+
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalogFS)
316+
if err != nil {
317+
httpError(w, err)
318+
return
319+
}
320+
321+
// Execute GraphQL query
322+
result := graphql.Do(graphql.Params{
323+
Schema: dynamicSchema.Schema,
324+
RequestString: params.Query,
325+
})
326+
327+
w.Header().Set("Content-Type", "application/json")
328+
if err := json.NewEncoder(w).Encode(result); err != nil {
329+
httpError(w, err)
330+
return
331+
}
332+
}
333+
272334
func (s *LocalDirV1) catalogData(catalog string) (*os.File, os.FileInfo, error) {
273335
catalogFile, err := os.Open(catalogFilePath(s.catalogDir(catalog)))
274336
if err != nil {
@@ -328,3 +390,50 @@ func (s *LocalDirV1) getIndex(catalog string) (*index, error) {
328390
}
329391
return idx.(*index), nil
330392
}
393+
394+
// createCatalogFS creates a filesystem interface for the catalog data
395+
func (s *LocalDirV1) createCatalogFS(catalog string) (fs.FS, error) {
396+
catalogDir := s.catalogDir(catalog)
397+
return os.DirFS(catalogDir), nil
398+
}
399+
400+
// buildCatalogGraphQLSchema builds a dynamic GraphQL schema for the given catalog
401+
func (s *LocalDirV1) buildCatalogGraphQLSchema(catalogFS fs.FS) (*gql.DynamicSchema, error) {
402+
var metas []*declcfg.Meta
403+
404+
// Collect all metas from the catalog filesystem
405+
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
406+
if err != nil {
407+
return err
408+
}
409+
if meta != nil {
410+
metas = append(metas, meta)
411+
}
412+
return nil
413+
})
414+
if err != nil {
415+
return nil, fmt.Errorf("error walking catalog metas: %w", err)
416+
}
417+
418+
// Discover schema from collected metas
419+
catalogSchema, err := gql.DiscoverSchemaFromMetas(metas)
420+
if err != nil {
421+
return nil, fmt.Errorf("error discovering schema: %w", err)
422+
}
423+
424+
// Organize metas by schema for resolvers
425+
metasBySchema := make(map[string][]*declcfg.Meta)
426+
for _, meta := range metas {
427+
if meta.Schema != "" {
428+
metasBySchema[meta.Schema] = append(metasBySchema[meta.Schema], meta)
429+
}
430+
}
431+
432+
// Build dynamic GraphQL schema
433+
dynamicSchema, err := gql.BuildDynamicGraphQLSchema(catalogSchema, metasBySchema)
434+
if err != nil {
435+
return nil, fmt.Errorf("error building GraphQL schema: %w", err)
436+
}
437+
438+
return dynamicSchema, nil
439+
}

0 commit comments

Comments
 (0)