Skip to content

Commit 888a0d6

Browse files
committed
schema-agnostic approaches with working tests
Signed-off-by: Jordan <jordan@nimblewidget.com>
1 parent 8684ac1 commit 888a0d6

File tree

18 files changed

+789
-524
lines changed

18 files changed

+789
-524
lines changed

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

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.3
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
@@ -142,7 +143,6 @@ require (
142143
github.com/google/uuid v1.6.0 // indirect
143144
github.com/gorilla/mux v1.8.1 // indirect
144145
github.com/gosuri/uitable v0.0.4 // indirect
145-
github.com/graphql-go/graphql v0.8.1 // indirect
146146
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
147147
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
148148
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,11 @@ curl -X POST http://localhost:8080/my-catalog/api/v1/graphql \
9292

9393
## Integration
9494

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

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

101101
## Technical Details
102102

internal/catalogd/graphql/discovery_test.go

Lines changed: 64 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -111,39 +111,33 @@ func TestDiscoverSchemaFromMetas_CoreLogic(t *testing.T) {
111111
bundleSchema, ok := catalogSchema.Schemas[declcfg.SchemaBundle]
112112
if !ok {
113113
t.Error("Bundle schema not discovered")
114-
} else {
115-
if bundleSchema.TotalObjects != 1 {
116-
t.Errorf("Expected 1 bundle object, got %d", bundleSchema.TotalObjects)
117-
}
114+
return
115+
}
118116

119-
// Check property types discovery
120-
if len(bundleSchema.PropertyTypes) == 0 {
121-
t.Error("No property types discovered for bundle schema")
122-
}
117+
if bundleSchema.TotalObjects != 1 {
118+
t.Errorf("Expected 1 bundle object, got %d", bundleSchema.TotalObjects)
119+
}
123120

124-
// Check for specific property types
125-
if olmPackage, exists := bundleSchema.PropertyTypes["olm.package"]; !exists {
126-
t.Error("olm.package property type not discovered")
127-
} else {
128-
expectedPropertyFields := []string{"packageName", "version"}
129-
for _, field := range expectedPropertyFields {
130-
graphqlField := remapFieldName(field)
131-
if _, exists := olmPackage[graphqlField]; !exists {
132-
t.Errorf("Expected property field %s not found in olm.package", graphqlField)
133-
}
134-
}
135-
}
121+
// Check that properties field is discovered with nested structure
122+
propertiesField, exists := bundleSchema.Fields[remapFieldName("properties")]
123+
if !exists {
124+
t.Error("properties field not discovered in bundle schema")
125+
return
126+
}
127+
if !propertiesField.IsArray {
128+
t.Error("properties field should be an array")
129+
return
130+
}
131+
if len(propertiesField.NestedFields) == 0 {
132+
t.Error("properties field should have nested fields discovered")
133+
return
134+
}
136135

137-
if olmGvk, exists := bundleSchema.PropertyTypes["olm.gvk"]; !exists {
138-
t.Error("olm.gvk property type not discovered")
139-
} else {
140-
expectedGvkFields := []string{"group", "version", "kind"}
141-
for _, field := range expectedGvkFields {
142-
graphqlField := remapFieldName(field)
143-
if _, exists := olmGvk[graphqlField]; !exists {
144-
t.Errorf("Expected GVK field %s not found in olm.gvk", graphqlField)
145-
}
146-
}
136+
// Check for typical property fields (type, value)
137+
expectedFields := []string{"type", "value"}
138+
for _, field := range expectedFields {
139+
if _, exists := propertiesField.NestedFields[remapFieldName(field)]; !exists {
140+
t.Errorf("Expected nested field %s not found in properties", field)
147141
}
148142
}
149143

@@ -178,7 +172,7 @@ func TestFieldNameRemapping_EdgeCases(t *testing.T) {
178172
{"operators.operatorframework.io/bundle.channels.v1", "operatorsOperatorframeworkIoBundleChannelsV1"},
179173
{"---", "field_"},
180174
{"123", "field_123"},
181-
{"field@#$%", "fieldField"},
175+
{"field@#$%", "field"},
182176
}
183177

184178
for _, tc := range testCases {
@@ -269,6 +263,7 @@ func TestAnalyzeJSONObject_FieldTypes(t *testing.T) {
269263
}
270264

271265
func TestBundlePropertiesAnalysis_ComprehensiveTypes(t *testing.T) {
266+
// Test that properties field is discovered with nested structure
272267
bundleObj := map[string]interface{}{
273268
"name": "test-bundle",
274269
"package": "test-package",
@@ -288,83 +283,40 @@ func TestBundlePropertiesAnalysis_ComprehensiveTypes(t *testing.T) {
288283
"kind": "TestResource",
289284
},
290285
},
291-
map[string]interface{}{
292-
"type": "olm.csv.metadata",
293-
"value": map[string]interface{}{
294-
"name": "test-operator",
295-
"namespace": "test-namespace",
296-
"annotations": map[string]interface{}{
297-
"description": "A test operator",
298-
},
299-
},
300-
},
301-
map[string]interface{}{
302-
"type": "olm.bundle.object",
303-
"value": map[string]interface{}{
304-
"ref": "objects/test.yaml",
305-
"data": map[string]interface{}{
306-
"apiVersion": "v1",
307-
"kind": "ConfigMap",
308-
"metadata": map[string]interface{}{
309-
"name": "config",
310-
},
311-
},
312-
},
313-
},
314286
},
315287
}
316288

317289
info := &SchemaInfo{
318-
PropertyTypes: make(map[string]map[string]*FieldInfo),
290+
Fields: make(map[string]*FieldInfo),
319291
}
320292

321-
analyzeBundleProperties(bundleObj, info)
293+
// Use the generic field analysis (not bundle-specific)
294+
analyzeJSONObject(bundleObj, info)
322295

323-
// Check that property types were discovered
324-
expectedPropertyTypes := []string{"olm.package", "olm.gvk", "olm.csv.metadata", "olm.bundle.object"}
325-
for _, propType := range expectedPropertyTypes {
326-
if _, exists := info.PropertyTypes[propType]; !exists {
327-
t.Errorf("Property type %s not discovered", propType)
328-
}
296+
// Check that properties field was discovered
297+
propertiesField, exists := info.Fields[remapFieldName("properties")]
298+
if !exists {
299+
t.Error("properties field not discovered")
300+
return
329301
}
330302

331-
// Check olm.package fields
332-
if olmPackage, exists := info.PropertyTypes["olm.package"]; exists {
333-
expectedFields := []string{"packageName", "version"}
334-
for _, field := range expectedFields {
335-
if _, exists := olmPackage[field]; !exists {
336-
t.Errorf("Field %s not found in olm.package property type", field)
337-
}
338-
}
339-
}
340-
341-
// Check olm.gvk fields
342-
if olmGvk, exists := info.PropertyTypes["olm.gvk"]; exists {
343-
expectedFields := []string{"group", "version", "kind"}
344-
for _, field := range expectedFields {
345-
if _, exists := olmGvk[field]; !exists {
346-
t.Errorf("Field %s not found in olm.gvk property type", field)
347-
}
348-
}
303+
// Verify it's detected as an array
304+
if !propertiesField.IsArray {
305+
t.Error("properties field should be detected as an array")
349306
}
350307

351-
// Check that nested objects are handled (annotations in csv.metadata)
352-
if csvMetadata, exists := info.PropertyTypes["olm.csv.metadata"]; exists {
353-
expectedFields := []string{"name", "namespace", "annotations"}
354-
for _, field := range expectedFields {
355-
if _, exists := csvMetadata[field]; !exists {
356-
t.Errorf("Field %s not found in olm.csv.metadata property type", field)
357-
}
358-
}
308+
// Verify nested fields were discovered
309+
if propertiesField.NestedFields == nil {
310+
t.Error("properties field should have nested fields discovered")
311+
return
359312
}
360313

361-
// Check bundle object type
362-
if bundleObject, exists := info.PropertyTypes["olm.bundle.object"]; exists {
363-
expectedFields := []string{"ref", "data"}
364-
for _, field := range expectedFields {
365-
if _, exists := bundleObject[field]; !exists {
366-
t.Errorf("Field %s not found in olm.bundle.object property type", field)
367-
}
314+
// Check for common property fields (type, value)
315+
expectedFields := []string{"type", "value"}
316+
for _, field := range expectedFields {
317+
fieldName := remapFieldName(field)
318+
if _, exists := propertiesField.NestedFields[fieldName]; !exists {
319+
t.Errorf("Expected nested field %s not found in properties", fieldName)
368320
}
369321
}
370322
}
@@ -458,22 +410,25 @@ func TestSchemaDiscovery_RealWorldExample(t *testing.T) {
458410
t.Fatal("Bundle schema not found")
459411
}
460412

461-
expectedPropertyTypes := map[string][]string{
462-
"olm.package": {"packageName", "version"},
463-
"olm.gvk": {"group", "kind", "version"},
464-
"olm.bundle.mediatype": {}, // This is a string value, no nested fields
413+
// With the schema-agnostic approach, we verify the properties field has nested structure
414+
propertiesField, exists := bundleSchema.Fields[remapFieldName("properties")]
415+
if !exists {
416+
t.Error("properties field not found in bundle schema")
417+
return
465418
}
466419

467-
for propType, expectedFields := range expectedPropertyTypes {
468-
if propFields, exists := bundleSchema.PropertyTypes[propType]; exists {
469-
for _, expectedField := range expectedFields {
470-
if _, fieldExists := propFields[expectedField]; !fieldExists {
471-
t.Errorf("Expected field %s not found in property type %s", expectedField, propType)
472-
}
473-
}
474-
} else if len(expectedFields) > 0 {
475-
// Only error if we expected fields (mediatype is a string, so no fields expected)
476-
t.Errorf("Property type %s not discovered", propType)
420+
if !propertiesField.IsArray {
421+
t.Error("properties field should be an array")
422+
}
423+
if len(propertiesField.NestedFields) == 0 {
424+
t.Error("properties field should have nested fields")
425+
return
426+
}
427+
428+
// Verify common property fields
429+
for _, field := range []string{"type", "value"} {
430+
if _, exists := propertiesField.NestedFields[remapFieldName(field)]; !exists {
431+
t.Errorf("Expected field %s not found in properties", field)
477432
}
478433
}
479434

0 commit comments

Comments
 (0)