Skip to content

Conversation

@grokspawn
Copy link
Contributor

Description

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 15, 2025
@netlify
Copy link

netlify bot commented Jul 15, 2025

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit 346cf0d
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/691b25af266fb700089f22a8
😎 Deploy Preview https://deploy-preview-2100--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Jul 15, 2025

Codecov Report

❌ Patch coverage is 30.25641% with 408 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.24%. Comparing base (c06f27f) to head (58fbad3).

Files with missing lines Patch % Lines
internal/catalogd/graphql/graphql.go 32.47% 245 Missing and 17 partials ⚠️
internal/catalogd/server/handlers.go 21.69% 80 Missing and 3 partials ⚠️
internal/catalogd/service/graphql_service.go 14.00% 43 Missing ⚠️
internal/catalogd/server/http_helpers.go 0.00% 12 Missing ⚠️
internal/catalogd/storage/localdir.go 66.66% 7 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2100      +/-   ##
==========================================
- Coverage   74.30%   71.24%   -3.07%     
==========================================
  Files          91       94       +3     
  Lines        7083     7476     +393     
==========================================
+ Hits         5263     5326      +63     
- Misses       1405     1719     +314     
- Partials      415      431      +16     
Flag Coverage Δ
e2e 43.50% <7.86%> (-2.15%) ⬇️
experimental-e2e 45.99% <8.20%> (-2.41%) ⬇️
unit 55.97% <24.27%> (-2.63%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@perdasilva
Copy link
Contributor

closing this as stale - please reopen if needed

@perdasilva perdasilva closed this Sep 17, 2025
@grokspawn grokspawn reopened this Oct 28, 2025
@openshift-ci
Copy link

openshift-ci bot commented Oct 28, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign tmshort for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copilot AI review requested due to automatic review settings October 30, 2025 13:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request refactors the catalogd storage layer to introduce GraphQL support for querying catalog data. The changes extract HTTP handler logic into a separate server package, introduce a service layer for GraphQL schema management with caching, and replace direct struct initialization with a constructor pattern for LocalDirV1.

  • Introduces a new GraphQL endpoint at /api/v1/graphql with dynamic schema generation
  • Refactors HTTP handlers into a dedicated server package with cleaner separation of concerns
  • Adds a GraphQL service layer with schema caching to improve performance
  • Updates LocalDirV1 to use a constructor pattern (NewLocalDirV1) for proper initialization

Reviewed Changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/catalogd/storage/localdir.go Refactored to use constructor pattern, added GraphQL service integration, moved HTTP handlers to server package
internal/catalogd/storage/localdir_test.go Updated all test instantiations to use new NewLocalDirV1 constructor
internal/catalogd/storage/http_preconditions_check.go Removed (moved to server package with simplified implementation)
internal/catalogd/server/handlers.go New file implementing HTTP handlers extracted from storage layer
internal/catalogd/server/http_helpers.go New file with simplified HTTP precondition checking
internal/catalogd/service/graphql_service.go New GraphQL service with caching for schema generation
internal/catalogd/graphql/graphql.go New dynamic GraphQL schema generation implementation
internal/catalogd/graphql/graphql_test.go Tests for GraphQL schema discovery
internal/catalogd/graphql/discovery_test.go Additional comprehensive tests for schema discovery edge cases
internal/catalogd/graphql/sample-queries.txt Documentation of sample GraphQL queries
internal/catalogd/graphql/README.md Documentation for GraphQL integration
cmd/catalogd/main.go Updated to use NewLocalDirV1 constructor
go.mod, go.sum Added graphql-go/graphql dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Check If-Modified-Since
if r.Method == http.MethodGet || r.Method == http.MethodHead {
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
// The Date-Modified header truncates sub-second precision, so
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Date-Modified' to 'Last-Modified'. The HTTP header name is 'Last-Modified', not 'Date-Modified'.

Copilot uses AI. Check for mistakes.
// Check If-Unmodified-Since
if r.Method != http.MethodGet && r.Method != http.MethodHead {
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil {
// The Date-Modified header truncates sub-second precision, so
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Date-Modified' to 'Last-Modified'. The HTTP header name is 'Last-Modified', not 'Date-Modified'.

Copilot uses AI. Check for mistakes.
Comment on lines 208 to 212
// Allow POST requests only for GraphQL endpoint
if r.URL.Path != "" && len(r.URL.Path) >= 7 && r.URL.Path[len(r.URL.Path)-7:] != "graphql" && r.Method == http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string suffix check using r.URL.Path[len(r.URL.Path)-7:] is fragile and uses a magic number (7). This logic can fail if the path is shorter than 7 characters or doesn't end exactly with 'graphql'. Consider using strings.HasSuffix(r.URL.Path, \"graphql\") or checking against the actual GraphQL path pattern for more robust route matching.

Copilot uses AI. Check for mistakes.
code = http.StatusInternalServerError
}
// Log the actual error for debugging
fmt.Printf("HTTP Error %d: %v\n", code, err)
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using fmt.Printf for error logging bypasses structured logging and may not be captured in production logging systems. Consider using a proper logger (e.g., from a logging library or context) to ensure errors are properly tracked and monitored.

Suggested change
fmt.Printf("HTTP Error %d: %v\n", code, err)
log.Printf("HTTP Error %d: %v\n", code, err)

Copilot uses AI. Check for mistakes.
m sync.RWMutex
// this singleflight Group is used in `getIndex()`` to handle concurrent HTTP requests
// optimally. With the use of this slightflight group, the index is loaded from disk
// this singleflight Group is used in `getIndex()` to handle concurrent HTTP requests
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment refers to getIndex() but the method has been renamed to GetIndex(). Update the comment to reference the correct method name.

Suggested change
// this singleflight Group is used in `getIndex()` to handle concurrent HTTP requests
// this singleflight Group is used in `GetIndex()` to handle concurrent HTTP requests

Copilot uses AI. Check for mistakes.
Comment on lines 265 to 271
// appendUnique adds a value to slice if not already present
func appendUnique(slice []interface{}, value interface{}) []interface{} {
for _, existing := range slice {
if reflect.DeepEqual(existing, value) {
return slice
}
}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The appendUnique function uses reflect.DeepEqual which can be expensive, especially when called repeatedly during schema discovery. Consider using a simpler equality check or a map-based approach if the performance impact is significant for large catalogs.

Suggested change
// appendUnique adds a value to slice if not already present
func appendUnique(slice []interface{}, value interface{}) []interface{} {
for _, existing := range slice {
if reflect.DeepEqual(existing, value) {
return slice
}
}
// appendUnique adds a value to slice if not already present, using JSON string as key for uniqueness
func appendUnique(slice []interface{}, value interface{}) []interface{} {
seen := make(map[string]struct{}, len(slice))
for _, existing := range slice {
key, err := json.Marshal(existing)
if err != nil {
continue // skip values that can't be marshaled
}
seen[string(key)] = struct{}{}
}
valueKey, err := json.Marshal(value)
if err != nil {
return slice // skip value if it can't be marshaled
}
if _, exists := seen[string(valueKey)]; exists {
return slice
}

Copilot uses AI. Check for mistakes.
Signed-off-by: grokspawn <jordan@nimblewidget.com>
Copilot AI review requested due to automatic review settings November 17, 2025 13:39
Copilot finished reviewing on behalf of grokspawn November 17, 2025 13:42
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +218
// Allow POST requests only for GraphQL endpoint
if !strings.HasSuffix(r.URL.Path, "graphql") && r.Method == http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
if !allowedMethodSet.Has(r.Method) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowedMethodsHandler function allows POST for all handlers if the path contains "graphql", but this check uses strings.HasSuffix(r.URL.Path, "graphql") which will match any path ending with "graphql". This logic is inverted - POST should only be allowed for GraphQL endpoints, not blocked. The current implementation at line 211 blocks POST for non-GraphQL endpoints (correct), but line 215 then checks if the method is in the allowed set which includes POST (line 69), creating confusion. Consider simplifying the logic to be more explicit about which methods are allowed for which endpoints.

Suggested change
// Allow POST requests only for GraphQL endpoint
if !strings.HasSuffix(r.URL.Path, "graphql") && r.Method == http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
if !allowedMethodSet.Has(r.Method) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
// Only allow POST for the GraphQL endpoint
if r.Method == http.MethodPost {
// Match exact GraphQL endpoint path (adjust as needed for your routing)
if !(strings.HasSuffix(r.URL.Path, "/graphql") && allowedMethodSet.Has(http.MethodPost)) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
} else if !allowedMethodSet.Has(r.Method) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
// Get or build the schema
// TODO: prevent cache rebuild on this callpath
dynamicSchema, err := s.GetSchema(catalog, catalogFS)
if err != nil {
return nil, fmt.Errorf("failed to get GraphQL schema: %w", err)
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment "prevent cache rebuild on this callpath" suggests a known performance issue where the schema cache might be rebuilt unnecessarily. This should be addressed before merging, or tracked with a specific issue reference.

Suggested change
// Get or build the schema
// TODO: prevent cache rebuild on this callpath
dynamicSchema, err := s.GetSchema(catalog, catalogFS)
if err != nil {
return nil, fmt.Errorf("failed to get GraphQL schema: %w", err)
// Get the schema from cache if available, otherwise build and cache it
s.schemaMux.RLock()
dynamicSchema, ok := s.schemaCache[catalog]
s.schemaMux.RUnlock()
if !ok {
var err error
dynamicSchema, err = s.GetSchema(catalog, catalogFS)
if err != nil {
return nil, fmt.Errorf("failed to get GraphQL schema: %w", err)
}

Copilot uses AI. Check for mistakes.
test func(*testing.T, *LocalDirV1, fs.FS)
cleanup func(*testing.T, *LocalDirV1)
}{
{
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case name "TestLocalDirStoraget" has a typo - it should be "TestLocalDirStorage" (missing the final 'e').

Copilot uses AI. Check for mistakes.
s.graphqlSvc.InvalidateCache(catalog)

// Pre-warm the GraphQL schema cache
if _, err := s.graphqlSvc.GetSchema(catalog, fsys); err != nil {
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-warming the GraphQL schema cache during Store() operation (lines 127-130) happens after the catalog has been successfully stored. If schema building fails, it returns an error, but the catalog data has already been persisted. This could leave the system in an inconsistent state where catalog data exists but has no GraphQL schema. Consider whether this error should be non-fatal or if the catalog storage should be rolled back on schema build failure.

Suggested change
if _, err := s.graphqlSvc.GetSchema(catalog, fsys); err != nil {
if _, err := s.graphqlSvc.GetSchema(catalog, fsys); err != nil {
// Rollback: remove catalogDir if schema build fails
_ = os.RemoveAll(catalogDir)

Copilot uses AI. Check for mistakes.
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil {
// The Last-Modified header truncates sub-second precision, so
// use ModTime >= t+1s instead of ModTime > t to check for modified.
if modtime.After(t.Add(-time.Second)) {
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkPreconditions function has incorrect logic for checking If-Unmodified-Since. Line 28 checks modtime.After(t.Add(-time.Second)) which adds negative one second to the request time, but the comment says "use ModTime >= t+1s instead of ModTime > t". The logic should be modtime.After(t) or !modtime.Before(t.Add(time.Second)) to properly implement the RFC 7232 semantics. The current implementation will incorrectly trigger precondition failures.

Suggested change
if modtime.After(t.Add(-time.Second)) {
if !modtime.Before(t.Add(time.Second)) {

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +102

// Collect all metas from the catalog filesystem
// WalkMetasFS walks the filesystem concurrently, so we need to protect the metas slice
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
if err != nil {
return err
}
if meta != nil {
metasMux.Lock()
metas = append(metas, meta)
metasMux.Unlock()
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildSchemaFromFS function uses declcfg.WalkMetasFS which walks the filesystem concurrently, requiring mutex protection for the metas slice (lines 91, 100-102). However, there's no protection for potential errors during concurrent execution. If one goroutine encounters an error, others may still be appending to the metas slice, leading to potential inconsistencies. Consider if error handling needs improvement here.

Suggested change
// Collect all metas from the catalog filesystem
// WalkMetasFS walks the filesystem concurrently, so we need to protect the metas slice
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
if err != nil {
return err
}
if meta != nil {
metasMux.Lock()
metas = append(metas, meta)
metasMux.Unlock()
var walkErr error
// Collect all metas from the catalog filesystem
// WalkMetasFS walks the filesystem concurrently, so we need to protect the metas slice and error
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
metasMux.Lock()
defer metasMux.Unlock()
if err != nil {
// Set shared error so other goroutines can check
if walkErr == nil {
walkErr = err
}
return err
}
// If an error has already occurred, skip further mutation
if walkErr != nil {
return walkErr
}
if meta != nil {
metas = append(metas, meta)

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +78
// If we have an empty part after having content, it means there was a trailing separator
// Add a capitalized version of the last word
if hasContent && i == len(parts)-1 {
// Get the base word (first non-empty part)
for _, p := range parts {
if p != "" {
result += strings.ToUpper(string(p[0])) + strings.ToLower(p[1:])
break
}
}
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field remapping logic in remapFieldName has complex handling for trailing underscores (lines 68-78) that seems incorrect. When an empty part is found at the end of the parts array (line 68), it tries to capitalize and append a word, but this logic appears to duplicate parts of field names rather than properly handle trailing separators. For example, a field ending with an underscore might get unexpected capitalization. This needs clarification or simplification.

Suggested change
// If we have an empty part after having content, it means there was a trailing separator
// Add a capitalized version of the last word
if hasContent && i == len(parts)-1 {
// Get the base word (first non-empty part)
for _, p := range parts {
if p != "" {
result += strings.ToUpper(string(p[0])) + strings.ToLower(p[1:])
break
}
}
}
// Skip empty parts (e.g., from trailing or consecutive underscores)

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +100
// Check for unexpected query parameters
expectedParams := map[string]bool{
"schema": true,
"package": true,
"name": true,
}

for param := range r.URL.Query() {
if !expectedParams[param] {
httpError(w, errInvalidParams)
return
}
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The check for unexpected query parameters in handleV1Metas validates against a hardcoded map (lines 89-93), but this validation logic could be more maintainable. Consider extracting this to a constant or using a more descriptive approach. This is a minor maintainability improvement.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +36
// checkPreconditions checks HTTP preconditions (If-Modified-Since, If-Unmodified-Since)
// Returns true if the request has already been handled (e.g., 304 Not Modified response sent)
func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool) {
// Check If-Modified-Since
if r.Method == http.MethodGet || r.Method == http.MethodHead {
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
// The Last-Modified header truncates sub-second precision, so
// use ModTime < t+1s instead of ModTime <= t to check for unmodified.
if modtime.Before(t.Add(time.Second)) {
w.WriteHeader(http.StatusNotModified)
return true
}
}
}

// Check If-Unmodified-Since
if r.Method != http.MethodGet && r.Method != http.MethodHead {
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil {
// The Last-Modified header truncates sub-second precision, so
// use ModTime >= t+1s instead of ModTime > t to check for modified.
if modtime.After(t.Add(-time.Second)) {
w.WriteHeader(http.StatusPreconditionFailed)
return true
}
}
}

return false
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplified checkPreconditions function removes support for ETag-based conditional requests (If-None-Match, If-Match) that were present in the deleted http_preconditions_check.go file. This is a breaking change that removes RFC 7232 compliance. The old implementation had comprehensive ETag support, while the new one only handles time-based preconditions. If this is intentional, it should be documented; otherwise, ETag support should be preserved.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +18
GraphQLCatalogQueries = featuregate.Feature("GraphQLCatalogQueries")
)

var catalogdFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
APIV1MetasHandler: {Default: false, PreRelease: featuregate.Alpha},
GraphQLCatalogQueries: {Default: false, PreRelease: featuregate.Alpha},
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature gate GraphQLCatalogQueries is defined in features.go but is never checked before enabling the GraphQL endpoint. The endpoint is unconditionally registered in handlers.go line 67, regardless of the feature gate setting. This means the feature gate has no effect. The feature gate should be checked in the handler registration logic or when creating the CatalogHandlers.

Copilot uses AI. Check for mistakes.
@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Nov 20, 2025
@openshift-merge-robot
Copy link

PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants