Skip to content

Commit 346cf0d

Browse files
committed
caehe on unpack; fix image verification policy
Signed-off-by: grokspawn <jordan@nimblewidget.com>
1 parent 58fbad3 commit 346cf0d

File tree

7 files changed

+57
-13
lines changed

7 files changed

+57
-13
lines changed

internal/catalogd/features/features.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010

1111
const (
1212
APIV1MetasHandler = featuregate.Feature("APIV1MetasHandler")
13+
GraphQLCatalogQueries = featuregate.Feature("GraphQLCatalogQueries")
1314
)
1415

1516
var catalogdFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
1617
APIV1MetasHandler: {Default: false, PreRelease: featuregate.Alpha},
18+
GraphQLCatalogQueries: {Default: false, PreRelease: featuregate.Alpha},
1719
}
1820

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

internal/catalogd/graphql/graphql.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,27 @@ func analyzeBundleProperties(obj map[string]interface{}, info *SchemaInfo) {
262262
}
263263
}
264264

265-
// appendUnique adds a value to slice if not already present
265+
// appendUnique adds a value to slice if not already present, using JSON string as key for uniqueness
266266
func appendUnique(slice []interface{}, value interface{}) []interface{} {
267+
seen := make(map[string]struct{}, len(slice))
268+
267269
for _, existing := range slice {
268-
if reflect.DeepEqual(existing, value) {
269-
return slice
270+
key, err := json.Marshal(existing)
271+
if err != nil {
272+
continue // skip values that can't be marshaled
270273
}
274+
seen[string(key)] = struct{}{}
275+
}
276+
277+
valueKey, err := json.Marshal(value)
278+
if err != nil {
279+
return slice // skip value if it can't be marshaled
280+
}
281+
282+
if _, exists := seen[string(valueKey)]; exists {
283+
return slice
271284
}
285+
272286
return append(slice, value)
273287
}
274288

internal/catalogd/server/handlers.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"net/http"
1010
"net/url"
1111
"os"
12+
"strings"
1213

1314
"k8s.io/apimachinery/pkg/util/sets"
15+
"k8s.io/klog/v2"
1416

1517
"github.com/operator-framework/operator-controller/internal/catalogd/service"
1618
)
@@ -21,10 +23,10 @@ const timeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
2123

2224
// CatalogHandlers handles HTTP requests for catalog content
2325
type CatalogHandlers struct {
24-
store CatalogStore
25-
graphqlSvc service.GraphQLService
26-
rootURL *url.URL
27-
enableMetas bool
26+
store CatalogStore
27+
graphqlSvc service.GraphQLService
28+
rootURL *url.URL
29+
enableMetas bool
2830
}
2931

3032
// Index provides methods for looking up catalog content by schema/package/name
@@ -183,7 +185,7 @@ func httpError(w http.ResponseWriter, err error) {
183185
code = http.StatusInternalServerError
184186
}
185187
// Log the actual error for debugging
186-
fmt.Printf("HTTP Error %d: %v\n", code, err)
188+
klog.ErrorS(err, "HTTP error", "code", code)
187189
http.Error(w, fmt.Sprintf("%d %s", code, http.StatusText(code)), code)
188190
}
189191

@@ -206,7 +208,7 @@ func allowedMethodsHandler(next http.Handler, allowedMethods ...string) http.Han
206208
allowedMethodSet := sets.New[string](allowedMethods...)
207209
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
208210
// Allow POST requests only for GraphQL endpoint
209-
if r.URL.Path != "" && len(r.URL.Path) >= 7 && r.URL.Path[len(r.URL.Path)-7:] != "graphql" && r.Method == http.MethodPost {
211+
if !strings.HasSuffix(r.URL.Path, "graphql") && r.Method == http.MethodPost {
210212
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
211213
return
212214
}

internal/catalogd/server/http_helpers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Tim
1111
// Check If-Modified-Since
1212
if r.Method == http.MethodGet || r.Method == http.MethodHead {
1313
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
14-
// The Date-Modified header truncates sub-second precision, so
14+
// The Last-Modified header truncates sub-second precision, so
1515
// use ModTime < t+1s instead of ModTime <= t to check for unmodified.
1616
if modtime.Before(t.Add(time.Second)) {
1717
w.WriteHeader(http.StatusNotModified)
@@ -23,7 +23,7 @@ func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Tim
2323
// Check If-Unmodified-Since
2424
if r.Method != http.MethodGet && r.Method != http.MethodHead {
2525
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil {
26-
// The Date-Modified header truncates sub-second precision, so
26+
// The Last-Modified header truncates sub-second precision, so
2727
// use ModTime >= t+1s instead of ModTime > t to check for modified.
2828
if modtime.After(t.Add(-time.Second)) {
2929
w.WriteHeader(http.StatusPreconditionFailed)

internal/catalogd/service/graphql_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func (s *CachedGraphQLService) GetSchema(catalog string, catalogFS fs.FS) (*gql.
6363
// ExecuteQuery executes a GraphQL query against a catalog
6464
func (s *CachedGraphQLService) ExecuteQuery(catalog string, catalogFS fs.FS, query string) (*graphql.Result, error) {
6565
// Get or build the schema
66+
// TODO: prevent cache rebuild on this callpath
6667
dynamicSchema, err := s.GetSchema(catalog, catalogFS)
6768
if err != nil {
6869
return nil, fmt.Errorf("failed to get GraphQL schema: %w", err)
@@ -87,14 +88,18 @@ func (s *CachedGraphQLService) InvalidateCache(catalog string) {
8788
// buildSchemaFromFS builds a GraphQL schema from a catalog filesystem
8889
func buildSchemaFromFS(catalogFS fs.FS) (*gql.DynamicSchema, error) {
8990
var metas []*declcfg.Meta
91+
var metasMux sync.Mutex
9092

9193
// Collect all metas from the catalog filesystem
94+
// WalkMetasFS walks the filesystem concurrently, so we need to protect the metas slice
9295
err := declcfg.WalkMetasFS(context.Background(), catalogFS, func(path string, meta *declcfg.Meta, err error) error {
9396
if err != nil {
9497
return err
9598
}
9699
if meta != nil {
100+
metasMux.Lock()
97101
metas = append(metas, meta)
102+
metasMux.Unlock()
98103
}
99104
return nil
100105
})

internal/catalogd/storage/localdir.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type LocalDirV1 struct {
3131
EnableMetasHandler bool
3232

3333
m sync.RWMutex
34-
// this singleflight Group is used in `getIndex()` to handle concurrent HTTP requests
34+
// this singleflight Group is used in `GetIndex()` to handle concurrent HTTP requests
3535
// optimally. With the use of this singleflight group, the index is loaded from disk
3636
// once per concurrent group of HTTP requests being handled by the metas handler.
3737
// The single flight instance gives us a way to load the index from disk exactly once
@@ -124,6 +124,11 @@ func (s *LocalDirV1) Store(ctx context.Context, catalog string, fsys fs.FS) erro
124124
// Invalidate GraphQL schema cache for this catalog
125125
s.graphqlSvc.InvalidateCache(catalog)
126126

127+
// Pre-warm the GraphQL schema cache
128+
if _, err := s.graphqlSvc.GetSchema(catalog, fsys); err != nil {
129+
return fmt.Errorf("failed to pre-build GraphQL schema for catalog %q, %v", catalog, err)
130+
}
131+
127132
return nil
128133
}
129134

internal/shared/util/image/pull_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ func TestContainersImagePuller_Pull(t *testing.T) {
4040
defer shutdown()
4141

4242
myModTime := time.Date(1985, 10, 25, 7, 53, 0, 0, time.FixedZone("PDT", -8*60*60))
43-
defaultContextFunc := func(context.Context) (*types.SystemContext, error) { return &types.SystemContext{}, nil }
43+
44+
// Create a default context with insecure policy for tests that don't use buildSourceContextFunc
45+
configDir := t.TempDir()
46+
policyPath := filepath.Join(configDir, "policy.json")
47+
insecurePolicy := `{"default":[{"type":"insecureAcceptAnything"}]}`
48+
require.NoError(t, os.WriteFile(policyPath, []byte(insecurePolicy), 0644))
49+
50+
defaultContextFunc := func(context.Context) (*types.SystemContext, error) {
51+
return &types.SystemContext{SignaturePolicyPath: policyPath}, nil
52+
}
4453

4554
testCases := []struct {
4655
name string
@@ -298,8 +307,15 @@ func buildSourceContextFunc(t *testing.T, ref reference.Named) func(context.Cont
298307
require.NoError(t, enc.Encode(registriesConf))
299308
require.NoError(t, f.Close())
300309

310+
// Create an insecure policy for testing to override any system-level policy
311+
// that might reject unsigned images
312+
policyPath := filepath.Join(configDir, "policy.json")
313+
insecurePolicy := `{"default":[{"type":"insecureAcceptAnything"}]}`
314+
require.NoError(t, os.WriteFile(policyPath, []byte(insecurePolicy), 0644))
315+
301316
return &types.SystemContext{
302317
SystemRegistriesConfPath: registriesConfPath,
318+
SignaturePolicyPath: policyPath,
303319
}, nil
304320
}
305321
}

0 commit comments

Comments
 (0)