Skip to content

Commit 99591f4

Browse files
committed
integration with existing http service endpoint
Signed-off-by: grokspawn <jordan@nimblewidget.com>
1 parent c4c4ee6 commit 99591f4

3 files changed

Lines changed: 225 additions & 37 deletions

File tree

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
@@ -18,6 +18,8 @@ import (
1818
"golang.org/x/sync/singleflight"
1919
"k8s.io/apimachinery/pkg/util/sets"
2020

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

@@ -225,9 +227,16 @@ func (s *LocalDirV1) StorageServerHandler() http.Handler {
225227
if s.EnableMetasHandler {
226228
mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "metas").Path, s.handleV1Metas)
227229
}
230+
mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "graphql").Path, s.handleV1GraphQL)
231+
228232
allowedMethodsHandler := func(next http.Handler, allowedMethods ...string) http.Handler {
229233
allowedMethodSet := sets.New[string](allowedMethods...)
230234
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
235+
// Allow POST requests for GraphQL endpoint
236+
if r.URL.Path != "" && r.URL.Path[len(r.URL.Path)-7:] == "graphql" && r.Method == http.MethodPost {
237+
next.ServeHTTP(w, r)
238+
return
239+
}
231240
if !allowedMethodSet.Has(r.Method) {
232241
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
233242
return
@@ -302,6 +311,59 @@ func (s *LocalDirV1) handleV1Metas(w http.ResponseWriter, r *http.Request) {
302311
serveJSONLines(w, r, indexReader)
303312
}
304313

314+
func (s *LocalDirV1) handleV1GraphQL(w http.ResponseWriter, r *http.Request) {
315+
s.m.RLock()
316+
defer s.m.RUnlock()
317+
318+
if r.Method != http.MethodPost {
319+
http.Error(w, "Only POST is allowed", http.StatusMethodNotAllowed)
320+
return
321+
}
322+
323+
catalog := r.PathValue("catalog")
324+
catalogFile, _, err := s.catalogData(catalog)
325+
if err != nil {
326+
httpError(w, err)
327+
return
328+
}
329+
defer catalogFile.Close()
330+
331+
// Parse GraphQL query from request body
332+
var params struct {
333+
Query string `json:"query"`
334+
}
335+
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
336+
http.Error(w, "Invalid request body", http.StatusBadRequest)
337+
return
338+
}
339+
340+
// Create catalog filesystem from the stored data
341+
catalogFS, err := s.createCatalogFS(catalog)
342+
if err != nil {
343+
httpError(w, err)
344+
return
345+
}
346+
347+
// Build dynamic GraphQL schema for this catalog
348+
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalogFS)
349+
if err != nil {
350+
httpError(w, err)
351+
return
352+
}
353+
354+
// Execute GraphQL query
355+
result := graphql.Do(graphql.Params{
356+
Schema: dynamicSchema.Schema,
357+
RequestString: params.Query,
358+
})
359+
360+
w.Header().Set("Content-Type", "application/json")
361+
if err := json.NewEncoder(w).Encode(result); err != nil {
362+
httpError(w, err)
363+
return
364+
}
365+
}
366+
305367
func (s *LocalDirV1) catalogData(catalog string) (*os.File, os.FileInfo, error) {
306368
catalogFile, err := os.Open(catalogFilePath(s.catalogDir(catalog)))
307369
if err != nil {
@@ -361,3 +423,50 @@ func (s *LocalDirV1) getIndex(catalog string) (*index, error) {
361423
}
362424
return idx.(*index), nil
363425
}
426+
427+
// createCatalogFS creates a filesystem interface for the catalog data
428+
func (s *LocalDirV1) createCatalogFS(catalog string) (fs.FS, error) {
429+
catalogDir := s.catalogDir(catalog)
430+
return os.DirFS(catalogDir), nil
431+
}
432+
433+
// buildCatalogGraphQLSchema builds a dynamic GraphQL schema for the given catalog
434+
func (s *LocalDirV1) buildCatalogGraphQLSchema(catalogFS fs.FS) (*gql.DynamicSchema, error) {
435+
var metas []*declcfg.Meta
436+
437+
// Collect all metas from the catalog filesystem
438+
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
439+
if err != nil {
440+
return err
441+
}
442+
if meta != nil {
443+
metas = append(metas, meta)
444+
}
445+
return nil
446+
})
447+
if err != nil {
448+
return nil, fmt.Errorf("error walking catalog metas: %w", err)
449+
}
450+
451+
// Discover schema from collected metas
452+
catalogSchema, err := gql.DiscoverSchemaFromMetas(metas)
453+
if err != nil {
454+
return nil, fmt.Errorf("error discovering schema: %w", err)
455+
}
456+
457+
// Organize metas by schema for resolvers
458+
metasBySchema := make(map[string][]*declcfg.Meta)
459+
for _, meta := range metas {
460+
if meta.Schema != "" {
461+
metasBySchema[meta.Schema] = append(metasBySchema[meta.Schema], meta)
462+
}
463+
}
464+
465+
// Build dynamic GraphQL schema
466+
dynamicSchema, err := gql.BuildDynamicGraphQLSchema(catalogSchema, metasBySchema)
467+
if err != nil {
468+
return nil, fmt.Errorf("error building GraphQL schema: %w", err)
469+
}
470+
471+
return dynamicSchema, nil
472+
}

0 commit comments

Comments
 (0)