Skip to content

Commit cd55ca7

Browse files
committed
schema-agnostic approaches with working tests
Signed-off-by: Jordan <jordan@nimblewidget.com>
1 parent 3c7e13b commit cd55ca7

19 files changed

Lines changed: 1338 additions & 654 deletions

cmd/catalogd/main.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,10 +365,25 @@ func run(ctx context.Context) error {
365365
return err
366366
}
367367

368+
var metasMode storage.MetasHandlerMode
369+
if features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler) {
370+
metasMode = storage.MetasHandlerEnabled
371+
} else {
372+
metasMode = storage.MetasHandlerDisabled
373+
}
374+
375+
var graphqlMode storage.GraphQLQueriesMode
376+
if features.CatalogdFeatureGate.Enabled(features.GraphQLCatalogQueries) {
377+
graphqlMode = storage.GraphQLQueriesEnabled
378+
} else {
379+
graphqlMode = storage.GraphQLQueriesDisabled
380+
}
381+
368382
localStorage = storage.NewLocalDirV1(
369383
storeDir,
370384
baseStorageURL,
371-
features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler),
385+
metasMode,
386+
graphqlMode,
372387
)
373388

374389
// Config for the catalogd web server
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Catalog queries using GraphQL
2+
3+
!!! warning "Alpha Feature"
4+
The GraphQL endpoint is an **alpha feature** controlled by the `GraphQLCatalogQueries` feature gate.
5+
The API and behavior may change in future releases.
6+
7+
After you [add a catalog of extensions](../../tutorials/add-catalog.md) to your cluster, you can query the catalog using GraphQL for flexible, structured queries with precise field selection.
8+
9+
## Prerequisites
10+
11+
* You have added a ClusterCatalog of extensions, such as [OperatorHub.io](https://operatorhub.io), to your cluster.
12+
* The `GraphQLCatalogQueries` feature gate is enabled in catalogd.
13+
14+
!!! note
15+
By default, Catalogd is installed with TLS enabled for the catalog webserver.
16+
The following examples will show this default behavior, but for simplicity's sake will ignore TLS verification in the curl commands using the `-k` flag.
17+
18+
You also need to port forward the catalog server service:
19+
20+
``` terminal
21+
kubectl -n olmv1-system port-forward svc/catalogd-service 8443:443
22+
```
23+
24+
## GraphQL Endpoint
25+
26+
The GraphQL endpoint is available at:
27+
28+
```
29+
https://localhost:8443/catalogs/<catalog-name>/api/v1/graphql
30+
```
31+
32+
All queries must be sent as **HTTP POST** requests with a JSON body containing a `query` field.
33+
34+
## Understanding GraphQL Field Names
35+
36+
**IMPORTANT**: GraphQL field names are automatically generated from catalog schema names.
37+
38+
### Naming Convention
39+
40+
Schema names are converted to GraphQL field names using this process:
41+
42+
1. Remove dots and special characters: `olm.bundle``olmbundle`
43+
2. Convert to lowercase: `OLM.Bundle``olmbundle`
44+
3. Append 's' for pluralization: `olmbundle``olmbundles`
45+
46+
**Examples:**
47+
48+
| Schema Name | GraphQL Field Name |
49+
|-------------|-------------------|
50+
| `olm.bundle` | `olmbundles` |
51+
| `olm.package` | `olmpackages` |
52+
| `olm.channel` | `olmchannels` |
53+
| `helm.chart` | `helmcharts` |
54+
55+
### Discovering Available Fields
56+
57+
To find the exact field names available for your catalog, use GraphQL introspection:
58+
59+
``` terminal
60+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
61+
-H "Content-Type: application/json" \
62+
-d '{
63+
"query": "{ __schema { queryType { fields { name description } } } }"
64+
}' | jq
65+
```
66+
67+
This returns all available query fields for the catalog, including the automatically generated schema-based fields.
68+
69+
!!! warning "Pluralization Limitations"
70+
The current implementation appends 's' to schema names for pluralization. This may not produce grammatically correct English plurals in all cases (e.g., `index``indexs` instead of `indices`). When creating custom schemas, use singular nouns that pluralize well with a simple 's' suffix.
71+
72+
## Basic Queries
73+
74+
### Catalog Summary
75+
76+
Get an overview of schemas and object counts in the catalog:
77+
78+
``` terminal
79+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
80+
-H "Content-Type: application/json" \
81+
-d '{
82+
"query": "{ summary { totalSchemas schemas { name totalObjects totalFields } } }"
83+
}' | jq
84+
```
85+
86+
### Query Bundles
87+
88+
List bundles with specific fields:
89+
90+
``` terminal
91+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
92+
-H "Content-Type: application/json" \
93+
-d '{
94+
"query": "{ olmbundles(limit: 5, offset: 0) { name package image } }"
95+
}' | jq
96+
```
97+
98+
### Query Packages
99+
100+
List packages with metadata:
101+
102+
``` terminal
103+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
104+
-H "Content-Type: application/json" \
105+
-d '{
106+
"query": "{ olmpackages(limit: 10) { name description defaultChannel } }"
107+
}' | jq
108+
```
109+
110+
### Query Channels
111+
112+
List channels:
113+
114+
``` terminal
115+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
116+
-H "Content-Type: application/json" \
117+
-d '{
118+
"query": "{ olmchannels { name package entries } }"
119+
}' | jq
120+
```
121+
122+
## Advanced Queries
123+
124+
### Pagination
125+
126+
All schema-based queries support pagination via `limit` and `offset` arguments:
127+
128+
``` terminal
129+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
130+
-H "Content-Type: application/json" \
131+
-d '{
132+
"query": "{ olmbundles(limit: 10, offset: 20) { name } }"
133+
}' | jq
134+
```
135+
136+
### Nested Field Selection
137+
138+
Select only the fields you need, including nested objects:
139+
140+
``` terminal
141+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
142+
-H "Content-Type: application/json" \
143+
-d '{
144+
"query": "{ olmpackages { name icon { mediatype base64data } } }"
145+
}' | jq
146+
```
147+
148+
### Complex Bundle Properties
149+
150+
Query bundle properties (which use union types for variable structures):
151+
152+
``` terminal
153+
curl -k -X POST 'https://localhost:8443/catalogs/operatorhubio/api/v1/graphql' \
154+
-H "Content-Type: application/json" \
155+
-d '{
156+
"query": "{ olmbundles(limit: 5) { name properties { type value } } }"
157+
}' | jq
158+
```
159+
160+
## Comparing GraphQL vs Metas Endpoint
161+
162+
| Feature | GraphQL (`/api/v1/graphql`) | Metas (`/api/v1/metas`) |
163+
|---------|---------------------------|------------------------|
164+
| Field selection | Precise - request only needed fields | All fields always returned |
165+
| Query complexity | Rich queries with nested objects | Simple parameter-based filtering |
166+
| Response size | Minimal - only requested data | Full objects always returned |
167+
| Schema discovery | Introspection built-in | External documentation needed |
168+
| Pagination | Built-in `limit` and `offset` | Manual implementation required |
169+
| HTTP Method | POST only | GET supported |
170+
| Feature status | Alpha (feature gate required) | Stable |
171+
172+
**When to use GraphQL:**
173+
- You need specific fields from large objects
174+
- You want to query related data in a single request
175+
- You need structured, typed responses
176+
- You're building a UI or client that benefits from precise data fetching
177+
178+
**When to use Metas endpoint:**
179+
- You need simple, stable API
180+
- You're doing basic filtering by schema/package/name
181+
- You want to use GET requests for caching
182+
- You need guaranteed API stability
183+
184+
## Limitations
185+
186+
1. **Pluralization**: Schema names are pluralized by appending 's', which may not be grammatically correct for all words
187+
2. **Schema naming**: Full schema names (including namespace/prefix) are preserved in field names (`olm.bundle``olmbundles`, not `bundles`)
188+
3. **POST only**: GraphQL endpoint only accepts POST requests, unlike the metas endpoint which supports GET
189+
4. **Alpha stability**: API may change in future releases while in alpha
190+
191+
## Enabling the GraphQL Feature
192+
193+
The GraphQL endpoint is controlled by the `GraphQLCatalogQueries` feature gate. To enable it:
194+
195+
``` yaml
196+
args:
197+
- --feature-gates=GraphQLCatalogQueries=true
198+
```
199+
200+
See [enable webhook support](enable-webhook-support.md) for more details on configuring feature gates.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/google/go-containerregistry v0.21.4
1818
github.com/google/renameio/v2 v2.0.2
1919
github.com/gorilla/handlers v1.5.2
20+
github.com/graphql-go/graphql v0.8.1
2021
github.com/klauspost/compress v1.18.5
2122
github.com/opencontainers/go-digest v1.0.0
2223
github.com/opencontainers/image-spec v1.1.1
@@ -144,7 +145,6 @@ require (
144145
github.com/gorilla/mux v1.8.1 // indirect
145146
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
146147
github.com/gosuri/uitable v0.0.4 // indirect
147-
github.com/graphql-go/graphql v0.8.1 // indirect
148148
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
149149
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
150150
github.com/h2non/filetype v1.1.3 // indirect

helm/experimental.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ options:
2323
features:
2424
enabled:
2525
- APIV1MetasHandler
26+
- GraphQLCatalogQueries
2627
# This can be one of: standard or experimental
2728
featureSet: experimental

internal/catalogd/features/features.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import (
99
)
1010

1111
const (
12-
APIV1MetasHandler = featuregate.Feature("APIV1MetasHandler")
12+
APIV1MetasHandler = featuregate.Feature("APIV1MetasHandler")
1313
GraphQLCatalogQueries = featuregate.Feature("GraphQLCatalogQueries")
1414
)
1515

1616
var catalogdFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
17-
APIV1MetasHandler: {Default: false, PreRelease: featuregate.Alpha},
18-
GraphQLCatalogQueries: {Default: false, PreRelease: featuregate.Alpha},
17+
APIV1MetasHandler: {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
18+
GraphQLCatalogQueries: {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
1919
}
2020

2121
var CatalogdFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()

internal/catalogd/graphql/README.md

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
This package provides dynamic GraphQL schema generation for operator catalog data, integrated into the catalogd storage server.
44

5+
⚠️ **Alpha Feature**: This is an experimental feature controlled by the `GraphQLCatalogQueries` feature gate. See user documentation at `docs/draft/howto/catalog-queries-graphql-endpoint.md`.
6+
57
## Usage
68

79
The GraphQL endpoint is now available as part of the catalogd storage server at:
@@ -43,7 +45,7 @@ curl -X POST http://localhost:8080/my-catalog/api/v1/graphql \
4345
#### Get bundles with pagination:
4446
```graphql
4547
{
46-
bundles(limit: 5, offset: 0) {
48+
olmbundles(limit: 5, offset: 0) {
4749
name
4850
package
4951
version
@@ -54,17 +56,28 @@ curl -X POST http://localhost:8080/my-catalog/api/v1/graphql \
5456
#### Get packages:
5557
```graphql
5658
{
57-
packages(limit: 10) {
59+
olmpackages(limit: 10) {
5860
name
5961
description
6062
}
6163
}
6264
```
6365

66+
#### Get channels:
67+
```graphql
68+
{
69+
olmchannels(limit: 10) {
70+
name
71+
package
72+
entries
73+
}
74+
}
75+
```
76+
6477
#### Get bundle properties (union types):
6578
```graphql
6679
{
67-
bundles(limit: 5) {
80+
olmbundles(limit: 5) {
6881
name
6982
properties {
7083
type
@@ -92,15 +105,58 @@ curl -X POST http://localhost:8080/my-catalog/api/v1/graphql \
92105

93106
## Integration
94107

95-
The GraphQL functionality is integrated into the `LocalDirV1` storage handler in `internal/catalogd/storage/localdir.go`:
108+
The GraphQL functionality is integrated across multiple packages:
96109

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
110+
- `internal/catalogd/server/handlers.go`: `CatalogHandlers.handleV1GraphQL()` handles POST requests to the GraphQL endpoint
111+
- `internal/catalogd/storage/localdir.go`: `LocalDirV1.GetCatalogFS()` creates filesystem interface for catalog data
112+
- `internal/catalogd/service/graphql_service.go`: `GraphQLService.GetSchema()` and `buildSchemaFromFS()` build dynamic GraphQL schemas for specific catalogs
100113

101114
## Technical Details
102115

103116
- Uses `declcfg.WalkMetasFS` to discover schema structure
104117
- Generates GraphQL object types dynamically from discovered fields
105118
- Creates union types for bundle properties with variable structures
106-
- Supports all standard GraphQL features including introspection
119+
- Supports all standard GraphQL features including introspection
120+
121+
## Field Naming Conventions
122+
123+
### Schema to GraphQL Field Name Mapping
124+
125+
**IMPORTANT**: GraphQL field names are automatically generated from schema names using the following convention:
126+
127+
1. **Remove dots and special characters** - `olm.bundle` becomes `olmbundle`
128+
2. **Convert to lowercase** - `OLM.Bundle` becomes `olmbundle`
129+
3. **Append 's' for pluralization** - `olmbundle` becomes `olmbundles`
130+
131+
**Examples:**
132+
- `olm.bundle``olmbundles`
133+
- `olm.package``olmpackages`
134+
- `olm.channel``olmchannels`
135+
- `helm.chart``helmcharts`
136+
- `custom.operator``customoperators`
137+
138+
### Limitations and Considerations
139+
140+
⚠️ **Pluralization Limitations**: The current implementation blindly appends 's' to create plural field names. This approach has known limitations:
141+
142+
1. **English grammar rules not applied**: Words ending in 's', 'x', 'z', 'ch', 'sh' should use 'es', but currently just get 's' appended
143+
2. **Irregular plurals not supported**: Schema names like `person`, `child`, `index` will become `persons`, `childs`, `indexs` instead of proper English plurals
144+
3. **Non-English schema names**: Schemas using non-English words will not follow appropriate pluralization rules for their language
145+
4. **Already-plural names**: If a schema name is already plural, it will still get 's' appended
146+
147+
**Recommendations for schema naming:**
148+
- Use schema names that work well with simple 's' pluralization (e.g., `bundle`, `package`, `chart`)
149+
- Avoid schema names that are already plural or have irregular plural forms
150+
- Document the expected GraphQL field names in your catalog documentation
151+
- Use GraphQL introspection to discover actual field names: `{ __schema { queryType { fields { name } } } }`
152+
153+
### Field Name Sanitization
154+
155+
All field names within objects are sanitized to be valid GraphQL identifiers:
156+
157+
- Special characters (dots, hyphens, etc.) are replaced with underscores
158+
- CamelCase conversion: `package-name``packageName`, `default_channel``defaultChannel`
159+
- Names starting with numbers get `field_` prefix: `123invalid``field_123invalid`
160+
- Empty or invalid names default to `value`
161+
162+
See `remapFieldName()` function for complete logic.

0 commit comments

Comments
 (0)