Skip to content

Chore: Introduce tests for the listener component #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions cmd/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
Expand Down Expand Up @@ -97,23 +98,32 @@ var listenCmd = &cobra.Command{
LeaderElectionID: "72231e1f.openmfp.io",
}

newMgrFunc := kcp.ManagerFactory(appCfg)
clt, err := client.New(cfg, client.Options{
Scheme: scheme,
})
if err != nil {
setupLog.Error(err, "failed to create client from config")
os.Exit(1)
}

mgr, err := newMgrFunc(cfg, mgrOpts)
mf := &kcp.ManagerFactory{
IsKCPEnabled: appCfg.EnableKcp,
}

mgr, err := mf.NewManager(cfg, mgrOpts, clt)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}

reconcilerOpts := kcp.ReconcilerOpts{
Scheme: scheme,
Client: clt,
Config: cfg,
OpenAPIDefinitionsPath: appCfg.OpenApiDefinitionsPath,
}

newReconcilerFunc := kcp.ReconcilerFactory(appCfg)

reconciler, err := newReconcilerFunc(reconcilerOpts)
reconciler, err := kcp.NewReconcilerFactory(appCfg).NewReconciler(reconcilerOpts)
if err != nil {
setupLog.Error(err, "unable to instantiate reconciler")
os.Exit(1)
Expand Down
4 changes: 4 additions & 0 deletions design_assets/Listener_High_Level.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions listener/apischema/crd_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ type GroupKindVersions struct {
}

type CRDResolver struct {
*discovery.DiscoveryClient
discovery.DiscoveryInterface
meta.RESTMapper
}

func (cr *CRDResolver) Resolve() ([]byte, error) {
return resolveSchema(cr.DiscoveryClient, cr.RESTMapper)
return resolveSchema(cr.DiscoveryInterface, cr.RESTMapper)
}

func (cr *CRDResolver) ResolveApiSchema(crd *apiextensionsv1.CustomResourceDefinition) ([]byte, error) {
Expand Down
60 changes: 41 additions & 19 deletions listener/clusterpath/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,44 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
errNilConfig = errors.New("config should not be nil")
errNilScheme = errors.New("scheme should not be nil")
)

type clientFactory func(config *rest.Config, options client.Options) (client.Client, error)

type Resolver struct {
*runtime.Scheme
*rest.Config
ResolverFunc
clientFactory
}

type ResolverFunc func(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, error)

func Resolve(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, error) {
if name == "root" {
return name, nil
}
func NewResolver(cfg *rest.Config, scheme *runtime.Scheme) (*Resolver, error) {
if cfg == nil {
return "", errors.New("config should not be nil")
return nil, errNilConfig
}
if scheme == nil {
return "", errors.New("scheme should not be nil")
return nil, errNilScheme
}
clusterCfg := rest.CopyConfig(cfg)
clusterCfgURL, err := url.Parse(clusterCfg.Host)
return &Resolver{
Scheme: scheme,
Config: cfg,
clientFactory: client.New,
}, nil
}

func (rf *Resolver) ClientForCluster(name string) (client.Client, error) {
clusterConfig, err := getClusterConfig(name, rf.Config)
if err != nil {
return "", fmt.Errorf("failed to parse rest config Host URL: %w", err)
return nil, fmt.Errorf("failed to get cluster config: %w", err)
}
clusterCfgURL.Path = fmt.Sprintf("/clusters/%s", name)
clusterCfg.Host = clusterCfgURL.String()
clt, err := client.New(clusterCfg, client.Options{
Scheme: scheme,
})
if err != nil {
return "", fmt.Errorf("failed to create client for cluster: %w", err)
return rf.clientFactory(clusterConfig, client.Options{Scheme: rf.Scheme})
}

func PathForCluster(name string, clt client.Client) (string, error) {
if name == "root" {
return name, nil
}
lc := &kcpcore.LogicalCluster{}
if err := clt.Get(context.TODO(), client.ObjectKey{Name: "cluster"}, lc); err != nil {
Expand All @@ -53,3 +61,17 @@ func Resolve(name string, cfg *rest.Config, scheme *runtime.Scheme) (string, err
}
return path, nil
}

func getClusterConfig(name string, cfg *rest.Config) (*rest.Config, error) {
if cfg == nil {
return nil, errors.New("config should not be nil")
}
clusterCfg := rest.CopyConfig(cfg)
clusterCfgURL, err := url.Parse(clusterCfg.Host)
if err != nil {
return nil, fmt.Errorf("failed to parse rest config's Host URL: %w", err)
}
clusterCfgURL.Path = fmt.Sprintf("/clusters/%s", name)
clusterCfg.Host = clusterCfgURL.String()
return clusterCfg, nil
}
157 changes: 157 additions & 0 deletions listener/clusterpath/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package clusterpath

import (
"net/url"
"testing"

kcpcore "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestResolver(t *testing.T) {
tests := map[string]struct {
baseConfig *rest.Config
clusterName string
expectErr bool
}{
"valid_cluster": {baseConfig: &rest.Config{}, clusterName: "test-cluster", expectErr: false},
"nil_base_config": {baseConfig: nil, clusterName: "test-cluster", expectErr: true},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
resolver := &Resolver{
Scheme: runtime.NewScheme(),
Config: tc.baseConfig,
clientFactory: func(config *rest.Config, options client.Options) (client.Client, error) {
return fake.NewClientBuilder().WithScheme(options.Scheme).Build(), nil
},
}

client, err := resolver.ClientForCluster(tc.clusterName)
if tc.expectErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, client)

})
}
}

func TestPathForCluster(t *testing.T) {
scheme := runtime.NewScheme()
err := kcpcore.AddToScheme(scheme)
assert.NoError(t, err)
tests := map[string]struct {
clusterName string
annotations map[string]string
expectErr bool
expectedPath string
}{
"root_cluster": {
clusterName: "root",
annotations: nil,
expectErr: false,
expectedPath: "root",
},
"valid_cluster_with_1st_level_path": {
clusterName: "sap",
annotations: map[string]string{"kcp.io/path": "root:sap"},
expectErr: false,
expectedPath: "root:sap",
},
"valid_cluster_with_2nd_level_path": {
clusterName: "openmfp",
annotations: map[string]string{"kcp.io/path": "root:sap:openmfp"},
expectErr: false,
expectedPath: "root:sap:openmfp",
},
"missing_annotation": {
clusterName: "test-cluster",
annotations: map[string]string{},
expectErr: true,
},
"nil_annotation": {
clusterName: "test-cluster",
annotations: nil,
expectErr: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
builder := fake.NewClientBuilder().WithScheme(scheme)
if tc.annotations != nil {
lc := &kcpcore.LogicalCluster{}
lc.SetName("cluster")
lc.SetAnnotations(tc.annotations)
builder = builder.WithObjects(lc)
}
clt := builder.Build()

path, err := PathForCluster(tc.clusterName, clt)
if tc.expectErr {
assert.Error(t, err)
assert.Empty(t, path)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.expectedPath, path)

})
}
}

func TestGetClusterConfig(t *testing.T) {
tests := map[string]struct {
cfg *rest.Config
cluster string
expect *rest.Config
expectErr bool
}{
"nil_config": {
cfg: nil,
cluster: "openmfp",
expect: nil,
expectErr: true,
},
"valid_config": {
cfg: &rest.Config{Host: "https://127.0.0.1:56120/clusters/root"},
cluster: "openmfp",
expect: &rest.Config{Host: "https://127.0.0.1:56120/clusters/openmfp"},
expectErr: false,
},
"invalid_URL": {
cfg: &rest.Config{Host: ":://bad-url"},
cluster: "openmfp",
expect: nil,
expectErr: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got, err := getClusterConfig(tc.cluster, tc.cfg)
if tc.expectErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Equal(t, tc.expect.Host, got.Host)
parsedURL, err1 := url.Parse(got.Host)
assert.NoError(t, err1)
assert.NotEmpty(t, parsedURL)
expectedURL, err2 := url.Parse(tc.expect.Host)
assert.NoError(t, err2)
assert.NotEmpty(t, expectedURL)
assert.Equal(t, expectedURL, parsedURL)
})
}
}
15 changes: 10 additions & 5 deletions listener/controller/apibinding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import (

// APIBindingReconciler reconciles an APIBinding object
type APIBindingReconciler struct {
io workspacefile.IOHandler
df discoveryclient.Factory
io *workspacefile.IOHandler
df *discoveryclient.Factory
sc apischema.Resolver
pr *clusterpath.Resolver
}

func NewAPIBindingReconciler(
io workspacefile.IOHandler,
df discoveryclient.Factory,
io *workspacefile.IOHandler,
df *discoveryclient.Factory,
sc apischema.Resolver,
pr *clusterpath.Resolver,
) *APIBindingReconciler {
Expand All @@ -48,7 +48,12 @@ func (r *APIBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}

logger := log.FromContext(ctx)
clusterPath, err := r.pr.ResolverFunc(req.ClusterName, r.pr.Config, r.pr.Scheme)
clusterClt, err := r.pr.ClientForCluster(req.ClusterName)
if err != nil {
logger.Error(err, "failed to get cluster client", "cluster", req.ClusterName)
return ctrl.Result{}, err
}
clusterPath, err := clusterpath.PathForCluster(req.ClusterName, clusterClt)
if err != nil {
logger.Error(err, "failed to get cluster path", "cluster", req.ClusterName)
return ctrl.Result{}, err
Expand Down
4 changes: 2 additions & 2 deletions listener/controller/crd_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ type CRDReconciler struct {
ClusterName string
client.Client
*apischema.CRDResolver
io workspacefile.IOHandler
io *workspacefile.IOHandler
}

func NewCRDReconciler(name string,
clt client.Client,
cr *apischema.CRDResolver,
io workspacefile.IOHandler,
io *workspacefile.IOHandler,
) *CRDReconciler {
return &CRDReconciler{
ClusterName: name,
Expand Down
Loading