Skip to content

Commit 0d2d4e4

Browse files
committed
schema-agnostic approaches with working tests
Signed-off-by: Jordan <jordan@nimblewidget.com>
1 parent 86fcc1f commit 0d2d4e4

File tree

16 files changed

+517
-523
lines changed

16 files changed

+517
-523
lines changed

cmd/catalogd/main.go

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

384+
var metasMode storage.MetasHandlerMode
385+
if features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler) {
386+
metasMode = storage.MetasHandlerEnabled
387+
} else {
388+
metasMode = storage.MetasHandlerDisabled
389+
}
390+
391+
var graphqlMode storage.GraphQLQueriesMode
392+
if features.CatalogdFeatureGate.Enabled(features.GraphQLCatalogQueries) {
393+
graphqlMode = storage.GraphQLQueriesEnabled
394+
} else {
395+
graphqlMode = storage.GraphQLQueriesDisabled
396+
}
397+
384398
localStorage = storage.NewLocalDirV1(
385399
storeDir,
386400
baseStorageURL,
387-
features.CatalogdFeatureGate.Enabled(features.APIV1MetasHandler),
401+
metasMode,
402+
graphqlMode,
388403
)
389404

390405
// 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: 52 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -116,35 +116,26 @@ func TestDiscoverSchemaFromMetas_CoreLogic(t *testing.T) {
116116
t.Errorf("Expected 1 bundle object, got %d", bundleSchema.TotalObjects)
117117
}
118118

119-
// Check property types discovery
120-
if len(bundleSchema.PropertyTypes) == 0 {
121-
t.Error("No property types discovered for bundle schema")
119+
// Check that properties field is discovered with nested structure
120+
propertiesField, exists := bundleSchema.Fields[remapFieldName("properties")]
121+
if !exists {
122+
t.Error("properties field not discovered in bundle schema")
123+
} else if !propertiesField.IsArray {
124+
t.Error("properties field should be an array")
125+
} else if propertiesField.NestedFields == nil || len(propertiesField.NestedFields) == 0 {
126+
t.Error("properties field should have nested fields discovered")
122127
}
123128

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)
129+
// Check for typical property fields (type, value)
130+
if propertiesField.NestedFields != nil {
131+
expectedFields := []string{"type", "value"}
132+
for _, field := range expectedFields {
133+
if _, exists := propertiesField.NestedFields[remapFieldName(field)]; !exists {
134+
t.Errorf("Expected nested field %s not found in properties", field)
133135
}
134136
}
135137
}
136138

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-
}
147-
}
148139
}
149140

150141
// Test channel schema
@@ -178,7 +169,7 @@ func TestFieldNameRemapping_EdgeCases(t *testing.T) {
178169
{"operators.operatorframework.io/bundle.channels.v1", "operatorsOperatorframeworkIoBundleChannelsV1"},
179170
{"---", "field_"},
180171
{"123", "field_123"},
181-
{"field@#$%", "fieldField"},
172+
{"field@#$%", "field"},
182173
}
183174

184175
for _, tc := range testCases {
@@ -269,6 +260,7 @@ func TestAnalyzeJSONObject_FieldTypes(t *testing.T) {
269260
}
270261

271262
func TestBundlePropertiesAnalysis_ComprehensiveTypes(t *testing.T) {
263+
// Test that properties field is discovered with nested structure
272264
bundleObj := map[string]interface{}{
273265
"name": "test-bundle",
274266
"package": "test-package",
@@ -288,83 +280,40 @@ func TestBundlePropertiesAnalysis_ComprehensiveTypes(t *testing.T) {
288280
"kind": "TestResource",
289281
},
290282
},
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-
},
314283
},
315284
}
316285

317286
info := &SchemaInfo{
318-
PropertyTypes: make(map[string]map[string]*FieldInfo),
287+
Fields: make(map[string]*FieldInfo),
319288
}
320289

321-
analyzeBundleProperties(bundleObj, info)
322-
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-
}
329-
}
290+
// Use the generic field analysis (not bundle-specific)
291+
analyzeJSONObject(bundleObj, info)
330292

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-
}
293+
// Check that properties field was discovered
294+
propertiesField, exists := info.Fields[remapFieldName("properties")]
295+
if !exists {
296+
t.Error("properties field not discovered")
297+
return
339298
}
340299

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-
}
300+
// Verify it's detected as an array
301+
if !propertiesField.IsArray {
302+
t.Error("properties field should be detected as an array")
349303
}
350304

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-
}
305+
// Verify nested fields were discovered
306+
if propertiesField.NestedFields == nil {
307+
t.Error("properties field should have nested fields discovered")
308+
return
359309
}
360310

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-
}
311+
// Check for common property fields (type, value)
312+
expectedFields := []string{"type", "value"}
313+
for _, field := range expectedFields {
314+
fieldName := remapFieldName(field)
315+
if _, exists := propertiesField.NestedFields[fieldName]; !exists {
316+
t.Errorf("Expected nested field %s not found in properties", fieldName)
368317
}
369318
}
370319
}
@@ -458,22 +407,23 @@ func TestSchemaDiscovery_RealWorldExample(t *testing.T) {
458407
t.Fatal("Bundle schema not found")
459408
}
460409

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
465-
}
466-
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)
410+
// With the schema-agnostic approach, we verify the properties field has nested structure
411+
propertiesField, exists := bundleSchema.Fields[remapFieldName("properties")]
412+
if !exists {
413+
t.Error("properties field not found in bundle schema")
414+
} else {
415+
if !propertiesField.IsArray {
416+
t.Error("properties field should be an array")
417+
}
418+
if propertiesField.NestedFields == nil || len(propertiesField.NestedFields) == 0 {
419+
t.Error("properties field should have nested fields")
420+
} else {
421+
// Verify common property fields
422+
for _, field := range []string{"type", "value"} {
423+
if _, exists := propertiesField.NestedFields[remapFieldName(field)]; !exists {
424+
t.Errorf("Expected field %s not found in properties", field)
472425
}
473426
}
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)
477427
}
478428
}
479429

0 commit comments

Comments
 (0)