From 5748039a9f71dc4c0737b6b8f431780c0d5c4921 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Fri, 2 Oct 2026 14:27:18 +0200 Subject: [PATCH 1/2] refactor(storage): add ListContainerProfiles to ProfileClient Signed-off-by: Matthias Bertschy --- .../containerprofilecache_test.go | 5 ++ .../host_shared_data_test.go | 5 ++ .../integration_helpers_test.go | 5 ++ .../reconciler_checksum_test.go | 17 +++++ .../reconciler_notfound_test.go | 5 ++ .../containerprofilecache/reconciler_test.go | 17 +++++ pkg/storage/storage_interface.go | 4 ++ pkg/storage/storage_mock.go | 18 +++++ pkg/storage/storage_mock_test.go | 69 +++++++++++++++++++ pkg/storage/v1/containerprofile.go | 6 ++ 10 files changed, 151 insertions(+) create mode 100644 pkg/storage/storage_mock_test.go diff --git a/pkg/objectcache/containerprofilecache/containerprofilecache_test.go b/pkg/objectcache/containerprofilecache/containerprofilecache_test.go index 72bda00154..28dbdf439c 100644 --- a/pkg/objectcache/containerprofilecache/containerprofilecache_test.go +++ b/pkg/objectcache/containerprofilecache/containerprofilecache_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -56,6 +57,10 @@ type fakeProfileClient struct { var _ storage.ProfileClient = (*fakeProfileClient)(nil) +func (f *fakeProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (f *fakeProfileClient) GetContainerProfile(_ context.Context, _, name string) (*v1beta1.ContainerProfile, error) { f.getCPCalls++ // Name-keyed authored CPs take precedence: this is how a multi-container pod diff --git a/pkg/objectcache/containerprofilecache/host_shared_data_test.go b/pkg/objectcache/containerprofilecache/host_shared_data_test.go index 6750c8f5f0..a3f6771681 100644 --- a/pkg/objectcache/containerprofilecache/host_shared_data_test.go +++ b/pkg/objectcache/containerprofilecache/host_shared_data_test.go @@ -15,6 +15,7 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // hostEventContainer builds the host pseudo-container event @@ -131,6 +132,10 @@ type signallingProfileClient struct { var _ storage.ProfileClient = (*signallingProfileClient)(nil) +func (c *signallingProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (c *signallingProfileClient) GetContainerProfile(_ context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { c.once.Do(func() { close(c.reached) }) return c.cp, nil diff --git a/pkg/objectcache/containerprofilecache/integration_helpers_test.go b/pkg/objectcache/containerprofilecache/integration_helpers_test.go index 6895a5813e..ee3e98fddb 100644 --- a/pkg/objectcache/containerprofilecache/integration_helpers_test.go +++ b/pkg/objectcache/containerprofilecache/integration_helpers_test.go @@ -12,6 +12,7 @@ import ( "github.com/kubescape/node-agent/pkg/storage" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -60,6 +61,10 @@ func newFakeStorage(cp *v1beta1.ContainerProfile) *stubStorage { return &stubStorage{cp: cp} } +func (s *stubStorage) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (s *stubStorage) GetContainerProfile(_ context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { s.mu.RLock() defer s.mu.RUnlock() diff --git a/pkg/objectcache/containerprofilecache/reconciler_checksum_test.go b/pkg/objectcache/containerprofilecache/reconciler_checksum_test.go index 6206e7a4fe..b1083223d2 100644 --- a/pkg/objectcache/containerprofilecache/reconciler_checksum_test.go +++ b/pkg/objectcache/containerprofilecache/reconciler_checksum_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -54,6 +55,10 @@ type checksumRecordingClient struct { var _ storage.ProfileClient = (*checksumRecordingClient)(nil) +func (c *checksumRecordingClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (c *checksumRecordingClient) GetContainerProfile(ctx context.Context, _, name string) (*v1beta1.ContainerProfile, error) { c.mu.Lock() c.fetches = append(c.fetches, recordedFetch{name: name, checksum: storage.KnownChecksumFromContext(ctx)}) @@ -387,6 +392,10 @@ type blockingSpecProfileClient struct { var _ storage.ProfileClient = (*blockingSpecProfileClient)(nil) +func (c *blockingSpecProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (c *blockingSpecProfileClient) GetContainerProfile(ctx context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { select { case c.entered <- struct{}{}: @@ -509,6 +518,10 @@ type conditionalChecksumClient struct { var _ storage.ProfileClient = (*conditionalChecksumClient)(nil) +func (c *conditionalChecksumClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (c *conditionalChecksumClient) GetContainerProfile(ctx context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { c.mu.Lock() defer c.mu.Unlock() @@ -966,6 +979,10 @@ type blockingFetchClient struct { var _ storage.ProfileClient = (*blockingFetchClient)(nil) +func (c *blockingFetchClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (c *blockingFetchClient) GetContainerProfile(ctx context.Context, _, name string) (*v1beta1.ContainerProfile, error) { active := c.active.Add(1) defer c.active.Add(-1) diff --git a/pkg/objectcache/containerprofilecache/reconciler_notfound_test.go b/pkg/objectcache/containerprofilecache/reconciler_notfound_test.go index c177a6ae21..e89493cb5a 100644 --- a/pkg/objectcache/containerprofilecache/reconciler_notfound_test.go +++ b/pkg/objectcache/containerprofilecache/reconciler_notfound_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -18,6 +19,10 @@ type scriptedProfileClient struct { errByName map[string]error } +func (s *scriptedProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (s *scriptedProfileClient) GetContainerProfile(_ context.Context, _, name string) (*v1beta1.ContainerProfile, error) { if err, ok := s.errByName[name]; ok { return nil, err diff --git a/pkg/objectcache/containerprofilecache/reconciler_test.go b/pkg/objectcache/containerprofilecache/reconciler_test.go index f591c0a772..6351fca7e0 100644 --- a/pkg/objectcache/containerprofilecache/reconciler_test.go +++ b/pkg/objectcache/containerprofilecache/reconciler_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ) @@ -76,6 +77,10 @@ type countingProfileClient struct { var _ storage.ProfileClient = (*countingProfileClient)(nil) +func (f *countingProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (f *countingProfileClient) GetContainerProfile(_ context.Context, _, name string) (*v1beta1.ContainerProfile, error) { f.cpCalls.Add(1) if f.userCP != nil && name == f.userCP.Name { @@ -568,6 +573,10 @@ type userCPErrorClient struct { var _ storage.ProfileClient = (*userCPErrorClient)(nil) +func (o *userCPErrorClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (o *userCPErrorClient) GetContainerProfile(_ context.Context, _, name string) (*v1beta1.ContainerProfile, error) { if name == o.userName { return nil, o.userCPErr @@ -613,6 +622,10 @@ type failingProfileClient struct { var _ storage.ProfileClient = (*failingProfileClient)(nil) +func (f *failingProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return nil, f.cpErr +} + func (f *failingProfileClient) GetContainerProfile(_ context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { return nil, f.cpErr } @@ -687,6 +700,10 @@ type blockingProfileClient struct { var _ storage.ProfileClient = (*blockingProfileClient)(nil) +func (b *blockingProfileClient) ListContainerProfiles(_ context.Context, _ string, _ metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return &v1beta1.ContainerProfileList{}, nil +} + func (b *blockingProfileClient) GetContainerProfile(ctx context.Context, _, _ string) (*v1beta1.ContainerProfile, error) { b.blocked <- struct{}{} // buffered(1): stored if reader hasn't arrived yet select { diff --git a/pkg/storage/storage_interface.go b/pkg/storage/storage_interface.go index 3c84c016e9..a8cca0b217 100644 --- a/pkg/storage/storage_interface.go +++ b/pkg/storage/storage_interface.go @@ -11,6 +11,10 @@ import ( type ProfileClient interface { GetContainerProfile(ctx context.Context, namespace, name string) (*v1beta1.ContainerProfile, error) + // ListContainerProfiles lists ContainerProfiles in a namespace, optionally + // filtered by a label selector — used to gather the fragments of a signed + // bundle (label signature.kubescape.io/bundle=). + ListContainerProfiles(ctx context.Context, namespace string, opts metav1.ListOptions) (*v1beta1.ContainerProfileList, error) } // ProfileCreator defines the interface for creating container profiles diff --git a/pkg/storage/storage_mock.go b/pkg/storage/storage_mock.go index f4d5403066..b388bdca8d 100644 --- a/pkg/storage/storage_mock.go +++ b/pkg/storage/storage_mock.go @@ -9,6 +9,7 @@ import ( spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" beta1 "github.com/kubescape/storage/pkg/generated/clientset/versioned/typed/softwarecomposition/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/watch" ) @@ -77,6 +78,23 @@ func (sc *StorageHttpClientMock) GetContainerProfile(_ context.Context, namespac return nil, nil } +func (sc *StorageHttpClientMock) ListContainerProfiles(_ context.Context, namespace string, opts metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + sc.containerProfilesMu.Lock() + defer sc.containerProfilesMu.Unlock() + sel, _ := labels.Parse(opts.LabelSelector) + out := &v1beta1.ContainerProfileList{} + for _, p := range sc.ContainerProfiles { + if p == nil || p.Namespace != namespace { + continue + } + if sel != nil && !sel.Matches(labels.Set(p.Labels)) { + continue + } + out.Items = append(out.Items, *p) + } + return out, nil +} + func (sc *StorageHttpClientMock) GetSBOMMeta(_ string) (*v1beta1.SBOMSyft, error) { return sc.mockSBOM, nil } diff --git a/pkg/storage/storage_mock_test.go b/pkg/storage/storage_mock_test.go new file mode 100644 index 0000000000..f15e833f2c --- /dev/null +++ b/pkg/storage/storage_mock_test.go @@ -0,0 +1,69 @@ +package storage + +import ( + "context" + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestStorageHttpClientMock_ListContainerProfiles(t *testing.T) { + mock := &StorageHttpClientMock{ + ContainerProfiles: []*v1beta1.ContainerProfile{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + Labels: map[string]string{ + "app": "frontend", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-2", + Namespace: "default", + Labels: map[string]string{ + "app": "backend", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-3", + Namespace: "other", + Labels: map[string]string{ + "app": "frontend", + }, + }, + }, + }, + } + + // 1. List all in default namespace + list, err := mock.ListContainerProfiles(context.Background(), "default", metav1.ListOptions{}) + require.NoError(t, err) + assert.Len(t, list.Items, 2) + + // 2. List with label selector in default namespace + list, err = mock.ListContainerProfiles(context.Background(), "default", metav1.ListOptions{ + LabelSelector: "app=frontend", + }) + require.NoError(t, err) + require.Len(t, list.Items, 1) + assert.Equal(t, "cp-1", list.Items[0].Name) + + // 3. List in other namespace + list, err = mock.ListContainerProfiles(context.Background(), "other", metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1) + assert.Equal(t, "cp-3", list.Items[0].Name) + + // 4. List in non-existent namespace + list, err = mock.ListContainerProfiles(context.Background(), "nonexistent", metav1.ListOptions{}) + require.NoError(t, err) + assert.Empty(t, list.Items) +} diff --git a/pkg/storage/v1/containerprofile.go b/pkg/storage/v1/containerprofile.go index 69fbc0ea5a..f02f13f446 100644 --- a/pkg/storage/v1/containerprofile.go +++ b/pkg/storage/v1/containerprofile.go @@ -11,6 +11,12 @@ func (sc *Storage) GetContainerProfile(ctx context.Context, namespace, name stri return sc.storageClient.ContainerProfiles(namespace).Get(ctx, name, metav1.GetOptions{}) } +// ListContainerProfiles lists ContainerProfiles in a namespace with the given +// options (typically a LabelSelector) — used to gather signed bundle fragments. +func (sc *Storage) ListContainerProfiles(ctx context.Context, namespace string, opts metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { + return sc.storageClient.ContainerProfiles(namespace).List(ctx, opts) +} + // CreateContainerProfileDirect directly creates the profile without queuing. // This implements the ProfileCreator interface. func (sc *Storage) CreateContainerProfileDirect(profile *v1beta1.ContainerProfile) error { From 4f9f3950f85f8a1aafa7c922d97b76f132aa5513 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Sat, 3 Oct 2026 10:14:25 +0200 Subject: [PATCH 2/2] fix(storage): address review comments from Copilot (5399673450) - propagate label selector parse errors in StorageHttpClientMock.ListContainerProfiles - preserve metav1.NamespaceAll cluster-wide listing behavior Signed-off-by: Matthias Bertschy --- pkg/storage/storage_mock.go | 9 ++++++--- pkg/storage/storage_mock_test.go | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/storage/storage_mock.go b/pkg/storage/storage_mock.go index b388bdca8d..712d69f0ef 100644 --- a/pkg/storage/storage_mock.go +++ b/pkg/storage/storage_mock.go @@ -81,13 +81,16 @@ func (sc *StorageHttpClientMock) GetContainerProfile(_ context.Context, namespac func (sc *StorageHttpClientMock) ListContainerProfiles(_ context.Context, namespace string, opts metav1.ListOptions) (*v1beta1.ContainerProfileList, error) { sc.containerProfilesMu.Lock() defer sc.containerProfilesMu.Unlock() - sel, _ := labels.Parse(opts.LabelSelector) + sel, err := labels.Parse(opts.LabelSelector) + if err != nil { + return nil, err + } out := &v1beta1.ContainerProfileList{} for _, p := range sc.ContainerProfiles { - if p == nil || p.Namespace != namespace { + if p == nil || (namespace != metav1.NamespaceAll && p.Namespace != namespace) { continue } - if sel != nil && !sel.Matches(labels.Set(p.Labels)) { + if !sel.Matches(labels.Set(p.Labels)) { continue } out.Items = append(out.Items, *p) diff --git a/pkg/storage/storage_mock_test.go b/pkg/storage/storage_mock_test.go index f15e833f2c..969e7f7c79 100644 --- a/pkg/storage/storage_mock_test.go +++ b/pkg/storage/storage_mock_test.go @@ -66,4 +66,22 @@ func TestStorageHttpClientMock_ListContainerProfiles(t *testing.T) { list, err = mock.ListContainerProfiles(context.Background(), "nonexistent", metav1.ListOptions{}) require.NoError(t, err) assert.Empty(t, list.Items) + + // 5. List cluster-wide across all namespaces (metav1.NamespaceAll / "") + list, err = mock.ListContainerProfiles(context.Background(), metav1.NamespaceAll, metav1.ListOptions{}) + require.NoError(t, err) + assert.Len(t, list.Items, 3) + + // 6. List cluster-wide with label selector + list, err = mock.ListContainerProfiles(context.Background(), metav1.NamespaceAll, metav1.ListOptions{ + LabelSelector: "app=frontend", + }) + require.NoError(t, err) + assert.Len(t, list.Items, 2) + + // 7. Invalid label selector returns error + _, err = mock.ListContainerProfiles(context.Background(), "default", metav1.ListOptions{ + LabelSelector: "invalid===selector", + }) + require.Error(t, err) }