Skip to content

Commit d5e8b62

Browse files
feat: component lister implementation for CTF (#912)
#### What this PR does / why we need it This PR provides component lister implementation for CTF archives. **This PR depends on** #911 and on bump of the `bindings/go/repository` module. #### Which issue(s) this PR fixes Contributes to open-component-model/ocm-project#673 --------- Signed-off-by: Ilya Khandamirov <ilya.khandamirov@sap.com> Signed-off-by: ikhandamirov <108289993+ikhandamirov@users.noreply.github.com> Co-authored-by: Jakob Möller <contact@jakob-moeller.com>
1 parent d8eabd2 commit d5e8b62

5 files changed

Lines changed: 288 additions & 2 deletions

File tree

bindings/go/oci/ctf/lister.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package ctf
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log/slog"
8+
"maps"
9+
"slices"
10+
"strings"
11+
12+
"ocm.software/open-component-model/bindings/go/ctf"
13+
ocipath "ocm.software/open-component-model/bindings/go/oci/spec/repository/path"
14+
repo "ocm.software/open-component-model/bindings/go/repository"
15+
)
16+
17+
// CTFComponentLister implements ComponentLister interface for CTF archives.
18+
// It does not support pagination and always returns the complete list of component names.
19+
type CTFComponentLister struct {
20+
// archive is the CTF store that is able to handle CTF contents.
21+
archive ctf.CTF
22+
}
23+
24+
var _ repo.ComponentLister = (*CTFComponentLister)(nil)
25+
26+
var ErrFnNil = errors.New("expected a valid callback function, but got nil")
27+
28+
// NewComponentLister creates a new ComponentLister for the given CTF archive.
29+
func NewComponentLister(archive ctf.CTF) *CTFComponentLister {
30+
lister := &CTFComponentLister{
31+
archive: archive,
32+
}
33+
34+
return lister
35+
}
36+
37+
// ListComponents lists all unique component names found in the CTF archive. List elements are lexically sorted.
38+
// The function does not support pagination and returns the complete list at once.
39+
// Thus, the `last` parameter is ignored.
40+
func (l *CTFComponentLister) ListComponents(ctx context.Context, last string, fn func(names []string) error) error {
41+
if fn == nil {
42+
return ErrFnNil
43+
}
44+
45+
if last != "" {
46+
logger := getLogger()
47+
logger.DebugContext(ctx, "pagination is not supported, ignoring 'last' parameter", "last", last)
48+
}
49+
50+
names, err := l.getAllNames(ctx)
51+
if err != nil {
52+
return fmt.Errorf("unable to list components: %w", err)
53+
}
54+
55+
return fn(names)
56+
}
57+
58+
func (l *CTFComponentLister) getAllNames(ctx context.Context) ([]string, error) {
59+
idx, err := l.archive.GetIndex(ctx)
60+
if err != nil {
61+
return nil, fmt.Errorf("unable to get CTF index: %w", err)
62+
}
63+
64+
arts := idx.GetArtifacts()
65+
if len(arts) == 0 {
66+
return nil, nil
67+
}
68+
69+
accumulatedNames := make(map[string]struct{})
70+
for _, art := range arts {
71+
// If repository starts with "component-descriptors/", the rest is the component name.
72+
prefix := ocipath.DefaultComponentDescriptorPath + "/"
73+
comp := art.Repository
74+
75+
if !strings.HasPrefix(comp, prefix) {
76+
continue
77+
}
78+
comp = strings.TrimPrefix(comp, prefix)
79+
accumulatedNames[comp] = struct{}{}
80+
}
81+
82+
nameList := slices.Collect(maps.Keys(accumulatedNames))
83+
slices.Sort(nameList)
84+
85+
return nameList, nil
86+
}
87+
88+
func getLogger() *slog.Logger {
89+
return slog.Default().With(slog.String("realm", "ctf-lister"))
90+
}

bindings/go/oci/ctf/lister_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package ctf
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"ocm.software/open-component-model/bindings/go/blob"
10+
"ocm.software/open-component-model/bindings/go/ctf"
11+
"ocm.software/open-component-model/bindings/go/ctf/index/v1"
12+
repo "ocm.software/open-component-model/bindings/go/repository"
13+
)
14+
15+
// MockCTF is a mock implementation of the CTF interface
16+
type MockCTF struct {
17+
// idx contains test data.
18+
idx v1.Index
19+
}
20+
21+
var _ ctf.CTF = (*MockCTF)(nil)
22+
23+
func TestListComponents(t *testing.T) {
24+
ctx := t.Context()
25+
testData := []string{
26+
"component-descriptors/componentC",
27+
"component-descriptors/componentB",
28+
"not-a-component",
29+
"component-descriptors/duplicate",
30+
"component-descriptors/componentD",
31+
"component-descriptors/componentA",
32+
"component-descriptors/duplicate",
33+
"not-a-component-again",
34+
}
35+
36+
tests := []struct {
37+
name string
38+
last string
39+
input []string
40+
expected []string
41+
}{
42+
{
43+
name: "default behavior - store order preserved",
44+
input: testData,
45+
expected: []string{"componentA", "componentB", "componentC", "componentD", "duplicate"},
46+
},
47+
{
48+
name: "last parameter should be ignored",
49+
last: "2",
50+
input: testData,
51+
expected: []string{"componentA", "componentB", "componentC", "componentD", "duplicate"},
52+
},
53+
{
54+
name: "single component in the store - one result",
55+
input: []string{"component-descriptors/componentA"},
56+
expected: []string{"componentA"},
57+
},
58+
{
59+
name: "empty store - empty result",
60+
input: []string{},
61+
expected: []string{},
62+
},
63+
{
64+
name: "store does not contain components - empty result",
65+
input: []string{"not-a-component", "not-a-component-again"},
66+
expected: []string{},
67+
},
68+
{
69+
name: "overlapping component names",
70+
input: []string{"component-descriptors/foo/bar", "component-descriptors/foo/bar/baz"},
71+
expected: []string{"foo/bar", "foo/bar/baz"},
72+
},
73+
}
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
// Create a mock CTF store with the test data.
77+
archive := NewMockCTF(tt.input)
78+
79+
// Create an instance of the CTFComponentLister.
80+
var lister repo.ComponentLister
81+
lister = NewComponentLister(archive)
82+
83+
// Collect the returned component names.
84+
result := []string{}
85+
err := lister.ListComponents(ctx, tt.last, func(names []string) error {
86+
result = append(result, names...)
87+
return nil
88+
})
89+
90+
assert.NoError(t, err)
91+
assert.Equal(t, tt.expected, result)
92+
})
93+
}
94+
}
95+
96+
func TestListComponentsFnNil(t *testing.T) {
97+
archive := NewMockCTF([]string{})
98+
var lister repo.ComponentLister
99+
lister = NewComponentLister(archive)
100+
err := lister.ListComponents(t.Context(), "", nil)
101+
assert.EqualError(t, err, ErrFnNil.Error())
102+
}
103+
104+
// NewMockCTF creates a new empty mock CTF.
105+
func NewMockCTF(compNames []string) *MockCTF {
106+
m := &MockCTF{}
107+
m.idx = v1.NewIndex()
108+
109+
for _, r := range compNames {
110+
a := v1.ArtifactMetadata{
111+
Repository: r,
112+
Tag: "v1",
113+
Digest: "sha256:abc",
114+
MediaType: "type1",
115+
}
116+
m.idx.AddArtifact(a)
117+
}
118+
119+
return m
120+
}
121+
122+
// Methods of the CTF interface.
123+
124+
// GetIndex is the only used method of the mock implementation.
125+
func (m *MockCTF) GetIndex(ctx context.Context) (v1.Index, error) {
126+
return m.idx, nil
127+
}
128+
129+
func (m *MockCTF) Format() ctf.FileFormat {
130+
panic("not implemented")
131+
}
132+
133+
func (m *MockCTF) SetIndex(ctx context.Context, index v1.Index) error {
134+
panic("not implemented")
135+
}
136+
137+
func (m *MockCTF) ListBlobs(ctx context.Context) ([]string, error) {
138+
panic("not implemented")
139+
}
140+
141+
func (m *MockCTF) GetBlob(ctx context.Context, digest string) (blob.ReadOnlyBlob, error) {
142+
panic("not implemented")
143+
}
144+
145+
func (m *MockCTF) SaveBlob(ctx context.Context, blob blob.ReadOnlyBlob) error {
146+
panic("not implemented")
147+
}
148+
149+
func (m *MockCTF) DeleteBlob(ctx context.Context, digest string) error {
150+
panic("not implemented")
151+
}

bindings/go/oci/ctf/store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package oci
1+
package ctf
22

33
import (
44
"context"

bindings/go/oci/ctf/store_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package oci
1+
package ctf
22

33
import (
44
"fmt"

bindings/go/oci/integration/integration_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,51 @@ func Test_Integration_CTF(t *testing.T) {
415415
})
416416
}
417417

418+
func Test_Integration_CTF_Lister(t *testing.T) {
419+
t.Parallel()
420+
421+
// Test data.
422+
cvs := []struct {
423+
name string
424+
version string
425+
}{
426+
{"github.com/acme.org/helloworld", "v1.0.0"},
427+
{"github.com/acme.org/helloworld", "v2.0.0"},
428+
{"github.com/acme.org/helloocm", "v1.0.0"},
429+
{"github.com/acme.org/hello-open-component-model", "v1.0.0"},
430+
}
431+
432+
// Expectation: sorted list, elements are unique.
433+
expectedList := []string{
434+
"github.com/acme.org/hello-open-component-model",
435+
"github.com/acme.org/helloocm",
436+
"github.com/acme.org/helloworld",
437+
}
438+
439+
// Write components to CTF, while validating that everything is written correctly.
440+
fs, err := filesystem.NewFS(t.TempDir(), os.O_RDWR)
441+
require.NoError(t, err)
442+
archive := ctf.NewFileSystemCTF(fs)
443+
store := ocictf.NewFromCTF(archive)
444+
repo, err := oci.NewRepository(oci.WithResolver(store), oci.WithTempDir(t.TempDir()))
445+
require.NoError(t, err)
446+
447+
for _, cv := range cvs {
448+
uploadDownloadBarebonesComponentVersion(t, repo, cv.name, cv.version)
449+
}
450+
451+
// Retrieve the component list and check the results.
452+
lister := ocictf.NewComponentLister(archive)
453+
result := []string{}
454+
err = lister.ListComponents(t.Context(), "", func(names []string) error {
455+
result = append(result, names...)
456+
return nil
457+
})
458+
459+
require.NoError(t, err)
460+
require.Equal(t, expectedList, result)
461+
}
462+
418463
func uploadDownloadLocalResourceOCILayout(t *testing.T, repo *oci.Repository, component string, version string) {
419464
ctx := t.Context()
420465
r := require.New(t)

0 commit comments

Comments
 (0)