Skip to content

Commit be28d8d

Browse files
feat(repository): path matching spec provider (#905)
<!-- markdownlint-disable MD041 --> #### What this PR does / why we need it Introduce a `ComponentVersionRepositorySpecProvider` as an alternative to the current `FallbackRepository` concept. This allows flexible configuration of which repository to use for resolving component versions, depending on the component's name - without the inefficient and potentially unstable fallback behavior. #### Which issue(s) this PR fixes <!-- Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Contributes to open-component-model/ocm-project#575 --------- Signed-off-by: Matthias Bruns <git@matthiasbruns.com> Signed-off-by: Fabian Burth <fabian.burth@sap.com> Co-authored-by: Matthias Bruns <git@matthiasbruns.com>
1 parent f602422 commit be28d8d

10 files changed

Lines changed: 247 additions & 654 deletions

File tree

bindings/go/repository/component/fallback/v1/doc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
// Deprecated: FallbackRepository is an implementation for the deprecated config
1313
// type "ocm.config.ocm.software/v1". This concept of fallback resolvers is deprecated
1414
// and only added for backwards compatibility.
15-
// An alternative concept is to use the [v1alpha1.ResolverRepository]
15+
// An alternative concept is to use the [v1alpha1.SpecProvider]
1616
package v1
1717

1818
import (
19-
v1 "ocm.software/open-component-model/bindings/go/repository/component/resolver/v1alpha1"
19+
"ocm.software/open-component-model/bindings/go/repository/component/pathmatcher/v1alpha1"
2020
)
2121

22-
var _ v1.ResolverRepository
22+
var _ v1alpha1.SpecProvider
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Package v1alpha1 provides a mechanism for selecting OCM (Open Component Model)
2+
// repository specifications based on component name patterns.
3+
//
4+
// It implements a ComponentVersionRepositorySpecProvider that uses path pattern
5+
// matching (from Go's standard library) in combination with the
6+
// resolver config type to associate component names with
7+
// repository specifications. This allows flexible configuration of which
8+
// repository to use for resolving component versions, depending on the
9+
// component's name.
10+
//
11+
// Example usage:
12+
//
13+
// provider := NewSpecProvider(ctx, resolvers)
14+
// repoSpec, err := provider.GetRepositorySpec(ctx, identity)
15+
//
16+
// This package is useful when you need to route component version requests to
17+
// different repositories based on naming conventions or patterns.
18+
package v1alpha1
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package v1alpha1
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"path"
7+
8+
resolverspec "ocm.software/open-component-model/bindings/go/configuration/resolvers/v1alpha1/spec"
9+
descruntime "ocm.software/open-component-model/bindings/go/descriptor/runtime"
10+
"ocm.software/open-component-model/bindings/go/runtime"
11+
)
12+
13+
// SpecProvider implements a ComponentVersionRepositorySpecProvider with
14+
// a resolver mechanism. It uses path patterns leveraging the go path standard
15+
// library to match component names to determine which OCM repository
16+
// specification to use for resolving component versions.
17+
type SpecProvider struct {
18+
// A list of resolvers to use for matching components to repositories.
19+
// This list is immutable after creation.
20+
resolvers []*resolverspec.Resolver
21+
}
22+
23+
// NewSpecProvider creates a new SpecProvider with a list of resolvers.
24+
// The resolvers are used to match component names to repository specifications.
25+
func NewSpecProvider(_ context.Context, resolvers []*resolverspec.Resolver) *SpecProvider {
26+
return &SpecProvider{
27+
resolvers: resolvers,
28+
}
29+
}
30+
31+
// GetRepositorySpec returns the repository specification for the given component identity.
32+
// It matches the component name against the configured resolvers and returns
33+
// the first matching repository specification.
34+
// If no matching resolver is found, an error is returned.
35+
// componentIdentity must contain the key [IdentityKey] containing the name of the component e.g. "ocm.software/core/test".
36+
func (r *SpecProvider) GetRepositorySpec(_ context.Context, componentIdentity runtime.Identity) (runtime.Typed, error) {
37+
componentName, ok := componentIdentity[descruntime.IdentityAttributeName]
38+
if !ok {
39+
return nil, fmt.Errorf("failed to extract component name from identity %s", componentIdentity)
40+
}
41+
42+
for index, resolver := range r.resolvers {
43+
ok, err := path.Match(resolver.ComponentNamePattern, componentName)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to match component name %q against pattern %q in resolver index %d: %w", componentName, resolver.ComponentNamePattern, index, err)
46+
}
47+
if ok {
48+
// Found a matching resolver, return its repository specification.
49+
// The caller is responsible for validating the specification.
50+
return resolver.Repository, nil
51+
}
52+
}
53+
54+
return nil, fmt.Errorf("no repository found for component identity %s", componentIdentity)
55+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package v1alpha1_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
resolverspec "ocm.software/open-component-model/bindings/go/configuration/resolvers/v1alpha1/spec"
9+
descruntime "ocm.software/open-component-model/bindings/go/descriptor/runtime"
10+
pathmatcher "ocm.software/open-component-model/bindings/go/repository/component/pathmatcher/v1alpha1"
11+
"ocm.software/open-component-model/bindings/go/runtime"
12+
)
13+
14+
func Test_ResolverRepository_GetRepositorySpec(t *testing.T) {
15+
ctx := t.Context()
16+
17+
cases := []struct {
18+
name string
19+
component string
20+
repos []*resolverspec.Resolver
21+
shouldReturnRep bool
22+
err assert.ErrorAssertionFunc
23+
}{
24+
{
25+
name: "test-component with no name",
26+
component: "",
27+
repos: []*resolverspec.Resolver{
28+
{
29+
Repository: &runtime.Raw{},
30+
ComponentNamePattern: "test-component",
31+
},
32+
},
33+
shouldReturnRep: false,
34+
err: func(t assert.TestingT, err error, i ...interface{}) bool {
35+
return assert.Error(t, err, "expected error when getting repository for spec")
36+
},
37+
},
38+
{
39+
name: "test-component with one version",
40+
component: "test-component",
41+
repos: []*resolverspec.Resolver{
42+
{
43+
Repository: &runtime.Raw{},
44+
ComponentNamePattern: "test-component",
45+
},
46+
},
47+
shouldReturnRep: true,
48+
err: assert.NoError,
49+
},
50+
{
51+
name: "test-component with no version",
52+
component: "test-component",
53+
repos: []*resolverspec.Resolver{
54+
{
55+
Repository: &runtime.Raw{},
56+
ComponentNamePattern: "test-component",
57+
},
58+
},
59+
shouldReturnRep: true,
60+
err: assert.NoError,
61+
},
62+
{
63+
name: "test-component with multiple repositories",
64+
component: "test-component",
65+
repos: []*resolverspec.Resolver{
66+
{
67+
Repository: &runtime.Raw{},
68+
ComponentNamePattern: "test-component",
69+
},
70+
{
71+
Repository: &runtime.Raw{},
72+
ComponentNamePattern: "repo2",
73+
},
74+
{
75+
Repository: &runtime.Raw{},
76+
ComponentNamePattern: "test-component",
77+
},
78+
},
79+
shouldReturnRep: true,
80+
err: assert.NoError,
81+
},
82+
{
83+
// glob component name pattern
84+
name: "glob pattern match",
85+
component: "ocm.software/core/test",
86+
repos: []*resolverspec.Resolver{
87+
{
88+
Repository: &runtime.Raw{},
89+
ComponentNamePattern: "ocm.software/core/*",
90+
},
91+
},
92+
shouldReturnRep: true,
93+
err: assert.NoError,
94+
},
95+
{
96+
// glob component name pattern no match
97+
name: "glob pattern no match",
98+
component: "ocm.software/other/test",
99+
repos: []*resolverspec.Resolver{
100+
{
101+
Repository: &runtime.Raw{},
102+
ComponentNamePattern: "ocm.software/core/*",
103+
},
104+
},
105+
shouldReturnRep: false,
106+
err: func(t assert.TestingT, err error, i ...interface{}) bool {
107+
return assert.Error(t, err, "expected error when getting repository for spec")
108+
},
109+
},
110+
// glob multiple wildcards
111+
{
112+
name: "glob pattern multiple wildcards match",
113+
component: "ocm.software/core/test",
114+
repos: []*resolverspec.Resolver{
115+
{
116+
Repository: &runtime.Raw{},
117+
ComponentNamePattern: "*.software/*/test",
118+
},
119+
},
120+
shouldReturnRep: true,
121+
err: assert.NoError,
122+
},
123+
{
124+
name: "multiple glob results",
125+
component: "ocm.software/core/test",
126+
repos: []*resolverspec.Resolver{
127+
{
128+
Repository: &runtime.Raw{},
129+
ComponentNamePattern: "ocm.software/*/test",
130+
},
131+
{
132+
Repository: &runtime.Raw{},
133+
ComponentNamePattern: "ocm.software/core/*",
134+
},
135+
},
136+
shouldReturnRep: true,
137+
err: assert.NoError,
138+
},
139+
}
140+
141+
for _, tc := range cases {
142+
t.Run(tc.name, func(t *testing.T) {
143+
r := require.New(t)
144+
145+
res := pathmatcher.NewSpecProvider(ctx, tc.repos)
146+
147+
identity := runtime.Identity{
148+
descruntime.IdentityAttributeName: tc.component,
149+
}
150+
151+
repo, err := res.GetRepositorySpec(ctx, identity)
152+
tc.err(t, err, "error getting repository for component")
153+
if tc.shouldReturnRep {
154+
r.NotNil(repo, "expected non-nil repository spec")
155+
} else {
156+
assert.Nil(t, repo, "expected nil repository spec")
157+
}
158+
})
159+
}
160+
}

bindings/go/repository/component/resolver/v1alpha1/doc.go

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)