Skip to content

Commit 4fb35ab

Browse files
authoredMar 13, 2025··
Chore: Introduce tests for the listener component (#83)
* add tests for listener On-behalf-of: @SAP a.shcherbatiuk@sap.com Signed-off-by: Artem Shcherbatiuk <vertex451@gmail.com>
1 parent b6c91c0 commit 4fb35ab

20 files changed

+813
-272
lines changed
 

‎cmd/listener.go

+15-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
1818
ctrl "sigs.k8s.io/controller-runtime"
19+
"sigs.k8s.io/controller-runtime/pkg/client"
1920
"sigs.k8s.io/controller-runtime/pkg/healthz"
2021
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2122
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
@@ -97,23 +98,32 @@ var listenCmd = &cobra.Command{
9798
LeaderElectionID: "72231e1f.openmfp.io",
9899
}
99100

100-
newMgrFunc := kcp.ManagerFactory(appCfg)
101+
clt, err := client.New(cfg, client.Options{
102+
Scheme: scheme,
103+
})
104+
if err != nil {
105+
setupLog.Error(err, "failed to create client from config")
106+
os.Exit(1)
107+
}
101108

102-
mgr, err := newMgrFunc(cfg, mgrOpts)
109+
mf := &kcp.ManagerFactory{
110+
IsKCPEnabled: appCfg.EnableKcp,
111+
}
112+
113+
mgr, err := mf.NewManager(cfg, mgrOpts, clt)
103114
if err != nil {
104115
setupLog.Error(err, "unable to start manager")
105116
os.Exit(1)
106117
}
107118

108119
reconcilerOpts := kcp.ReconcilerOpts{
109120
Scheme: scheme,
121+
Client: clt,
110122
Config: cfg,
111123
OpenAPIDefinitionsPath: appCfg.OpenApiDefinitionsPath,
112124
}
113125

114-
newReconcilerFunc := kcp.ReconcilerFactory(appCfg)
115-
116-
reconciler, err := newReconcilerFunc(reconcilerOpts)
126+
reconciler, err := kcp.NewReconcilerFactory(appCfg).NewReconciler(reconcilerOpts)
117127
if err != nil {
118128
setupLog.Error(err, "unable to instantiate reconciler")
119129
os.Exit(1)

‎design_assets/Listener_High_Level.drawio.svg

+4
Loading

‎listener/apischema/crd_resolver.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ type GroupKindVersions struct {
2929
}
3030

3131
type CRDResolver struct {
32-
*discovery.DiscoveryClient
32+
discovery.DiscoveryInterface
3333
meta.RESTMapper
3434
}
3535

3636
func (cr *CRDResolver) Resolve() ([]byte, error) {
37-
return resolveSchema(cr.DiscoveryClient, cr.RESTMapper)
37+
return resolveSchema(cr.DiscoveryInterface, cr.RESTMapper)
3838
}
3939

4040
func (cr *CRDResolver) ResolveApiSchema(crd *apiextensionsv1.CustomResourceDefinition) ([]byte, error) {

‎listener/clusterpath/resolver.go

+41-19
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,44 @@ import (
1212
"sigs.k8s.io/controller-runtime/pkg/client"
1313
)
1414

15+
var (
16+
errNilConfig = errors.New("config should not be nil")
17+
errNilScheme = errors.New("scheme should not be nil")
18+
)
19+
20+
type clientFactory func(config *rest.Config, options client.Options) (client.Client, error)
21+
1522
type Resolver struct {
1623
*runtime.Scheme
1724
*rest.Config
18-
ResolverFunc
25+
clientFactory
1926
}
2027

21-
type ResolverFunc func(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, error)
22-
23-
func Resolve(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, error) {
24-
if name == "root" {
25-
return name, nil
26-
}
28+
func NewResolver(cfg *rest.Config, scheme *runtime.Scheme) (*Resolver, error) {
2729
if cfg == nil {
28-
return "", errors.New("config should not be nil")
30+
return nil, errNilConfig
2931
}
3032
if scheme == nil {
31-
return "", errors.New("scheme should not be nil")
33+
return nil, errNilScheme
3234
}
33-
clusterCfg := rest.CopyConfig(cfg)
34-
clusterCfgURL, err := url.Parse(clusterCfg.Host)
35+
return &Resolver{
36+
Scheme: scheme,
37+
Config: cfg,
38+
clientFactory: client.New,
39+
}, nil
40+
}
41+
42+
func (rf *Resolver) ClientForCluster(name string) (client.Client, error) {
43+
clusterConfig, err := getClusterConfig(name, rf.Config)
3544
if err != nil {
36-
return "", fmt.Errorf("failed to parse rest config Host URL: %w", err)
45+
return nil, fmt.Errorf("failed to get cluster config: %w", err)
3746
}
38-
clusterCfgURL.Path = fmt.Sprintf("/clusters/%s", name)
39-
clusterCfg.Host = clusterCfgURL.String()
40-
clt, err := client.New(clusterCfg, client.Options{
41-
Scheme: scheme,
42-
})
43-
if err != nil {
44-
return "", fmt.Errorf("failed to create client for cluster: %w", err)
47+
return rf.clientFactory(clusterConfig, client.Options{Scheme: rf.Scheme})
48+
}
49+
50+
func PathForCluster(name string, clt client.Client) (string, error) {
51+
if name == "root" {
52+
return name, nil
4553
}
4654
lc := &kcpcore.LogicalCluster{}
4755
if err := clt.Get(context.TODO(), client.ObjectKey{Name: "cluster"}, lc); err != nil {
@@ -53,3 +61,17 @@ func Resolve(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, err
5361
}
5462
return path, nil
5563
}
64+
65+
func getClusterConfig(name string, cfg *rest.Config) (*rest.Config, error) {
66+
if cfg == nil {
67+
return nil, errors.New("config should not be nil")
68+
}
69+
clusterCfg := rest.CopyConfig(cfg)
70+
clusterCfgURL, err := url.Parse(clusterCfg.Host)
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to parse rest config's Host URL: %w", err)
73+
}
74+
clusterCfgURL.Path = fmt.Sprintf("/clusters/%s", name)
75+
clusterCfg.Host = clusterCfgURL.String()
76+
return clusterCfg, nil
77+
}

‎listener/clusterpath/resolver_test.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package clusterpath
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
kcpcore "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
8+
"github.com/stretchr/testify/assert"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/client-go/rest"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
13+
)
14+
15+
func TestResolver(t *testing.T) {
16+
tests := map[string]struct {
17+
baseConfig *rest.Config
18+
clusterName string
19+
expectErr bool
20+
}{
21+
"valid_cluster": {baseConfig: &rest.Config{}, clusterName: "test-cluster", expectErr: false},
22+
"nil_base_config": {baseConfig: nil, clusterName: "test-cluster", expectErr: true},
23+
}
24+
25+
for name, tc := range tests {
26+
t.Run(name, func(t *testing.T) {
27+
resolver := &Resolver{
28+
Scheme: runtime.NewScheme(),
29+
Config: tc.baseConfig,
30+
clientFactory: func(config *rest.Config, options client.Options) (client.Client, error) {
31+
return fake.NewClientBuilder().WithScheme(options.Scheme).Build(), nil
32+
},
33+
}
34+
35+
client, err := resolver.ClientForCluster(tc.clusterName)
36+
if tc.expectErr {
37+
assert.Error(t, err)
38+
return
39+
}
40+
assert.NoError(t, err)
41+
assert.NotNil(t, client)
42+
43+
})
44+
}
45+
}
46+
47+
func TestPathForCluster(t *testing.T) {
48+
scheme := runtime.NewScheme()
49+
err := kcpcore.AddToScheme(scheme)
50+
assert.NoError(t, err)
51+
tests := map[string]struct {
52+
clusterName string
53+
annotations map[string]string
54+
expectErr bool
55+
expectedPath string
56+
}{
57+
"root_cluster": {
58+
clusterName: "root",
59+
annotations: nil,
60+
expectErr: false,
61+
expectedPath: "root",
62+
},
63+
"valid_cluster_with_1st_level_path": {
64+
clusterName: "sap",
65+
annotations: map[string]string{"kcp.io/path": "root:sap"},
66+
expectErr: false,
67+
expectedPath: "root:sap",
68+
},
69+
"valid_cluster_with_2nd_level_path": {
70+
clusterName: "openmfp",
71+
annotations: map[string]string{"kcp.io/path": "root:sap:openmfp"},
72+
expectErr: false,
73+
expectedPath: "root:sap:openmfp",
74+
},
75+
"missing_annotation": {
76+
clusterName: "test-cluster",
77+
annotations: map[string]string{},
78+
expectErr: true,
79+
},
80+
"nil_annotation": {
81+
clusterName: "test-cluster",
82+
annotations: nil,
83+
expectErr: true,
84+
},
85+
}
86+
87+
for name, tc := range tests {
88+
t.Run(name, func(t *testing.T) {
89+
builder := fake.NewClientBuilder().WithScheme(scheme)
90+
if tc.annotations != nil {
91+
lc := &kcpcore.LogicalCluster{}
92+
lc.SetName("cluster")
93+
lc.SetAnnotations(tc.annotations)
94+
builder = builder.WithObjects(lc)
95+
}
96+
clt := builder.Build()
97+
98+
path, err := PathForCluster(tc.clusterName, clt)
99+
if tc.expectErr {
100+
assert.Error(t, err)
101+
assert.Empty(t, path)
102+
return
103+
}
104+
assert.NoError(t, err)
105+
assert.Equal(t, tc.expectedPath, path)
106+
107+
})
108+
}
109+
}
110+
111+
func TestGetClusterConfig(t *testing.T) {
112+
tests := map[string]struct {
113+
cfg *rest.Config
114+
cluster string
115+
expect *rest.Config
116+
expectErr bool
117+
}{
118+
"nil_config": {
119+
cfg: nil,
120+
cluster: "openmfp",
121+
expect: nil,
122+
expectErr: true,
123+
},
124+
"valid_config": {
125+
cfg: &rest.Config{Host: "https://127.0.0.1:56120/clusters/root"},
126+
cluster: "openmfp",
127+
expect: &rest.Config{Host: "https://127.0.0.1:56120/clusters/openmfp"},
128+
expectErr: false,
129+
},
130+
"invalid_URL": {
131+
cfg: &rest.Config{Host: ":://bad-url"},
132+
cluster: "openmfp",
133+
expect: nil,
134+
expectErr: true,
135+
},
136+
}
137+
138+
for name, tc := range tests {
139+
t.Run(name, func(t *testing.T) {
140+
got, err := getClusterConfig(tc.cluster, tc.cfg)
141+
if tc.expectErr {
142+
assert.Error(t, err)
143+
return
144+
}
145+
assert.NoError(t, err)
146+
assert.NotNil(t, got)
147+
assert.Equal(t, tc.expect.Host, got.Host)
148+
parsedURL, err1 := url.Parse(got.Host)
149+
assert.NoError(t, err1)
150+
assert.NotEmpty(t, parsedURL)
151+
expectedURL, err2 := url.Parse(tc.expect.Host)
152+
assert.NoError(t, err2)
153+
assert.NotEmpty(t, expectedURL)
154+
assert.Equal(t, expectedURL, parsedURL)
155+
})
156+
}
157+
}

‎listener/controller/apibinding_controller.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import (
2121

2222
// APIBindingReconciler reconciles an APIBinding object
2323
type APIBindingReconciler struct {
24-
io workspacefile.IOHandler
25-
df discoveryclient.Factory
24+
io *workspacefile.IOHandler
25+
df *discoveryclient.Factory
2626
sc apischema.Resolver
2727
pr *clusterpath.Resolver
2828
}
2929

3030
func NewAPIBindingReconciler(
31-
io workspacefile.IOHandler,
32-
df discoveryclient.Factory,
31+
io *workspacefile.IOHandler,
32+
df *discoveryclient.Factory,
3333
sc apischema.Resolver,
3434
pr *clusterpath.Resolver,
3535
) *APIBindingReconciler {
@@ -48,7 +48,12 @@ func (r *APIBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request)
4848
}
4949

5050
logger := log.FromContext(ctx)
51-
clusterPath, err := r.pr.ResolverFunc(req.ClusterName, r.pr.Config, r.pr.Scheme)
51+
clusterClt, err := r.pr.ClientForCluster(req.ClusterName)
52+
if err != nil {
53+
logger.Error(err, "failed to get cluster client", "cluster", req.ClusterName)
54+
return ctrl.Result{}, err
55+
}
56+
clusterPath, err := clusterpath.PathForCluster(req.ClusterName, clusterClt)
5257
if err != nil {
5358
logger.Error(err, "failed to get cluster path", "cluster", req.ClusterName)
5459
return ctrl.Result{}, err

‎listener/controller/crd_controller.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ type CRDReconciler struct {
2020
ClusterName string
2121
client.Client
2222
*apischema.CRDResolver
23-
io workspacefile.IOHandler
23+
io *workspacefile.IOHandler
2424
}
2525

2626
func NewCRDReconciler(name string,
2727
clt client.Client,
2828
cr *apischema.CRDResolver,
29-
io workspacefile.IOHandler,
29+
io *workspacefile.IOHandler,
3030
) *CRDReconciler {
3131
return &CRDReconciler{
3232
ClusterName: name,

0 commit comments

Comments
 (0)
Please sign in to comment.