Skip to content
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
2 changes: 2 additions & 0 deletions internal/clientfactory/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func NewClientFor(config *rest.Config, scheme *runtime.Scheme, name string) (*Cl
if err != nil {
return nil, err
}
// TODO: a full clientset which contains all builtin groups is actually not needed here;
// a clientset for corev1 events, plus discovery would be sufficient
clientset, err := kubernetes.NewForConfigAndClient(config, httpClient)
if err != nil {
return nil, err
Expand Down
71 changes: 71 additions & 0 deletions internal/clientfactory/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package clientfactory

import (
"context"
"fmt"
"time"

"github.com/sap/go-generics/slices"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("testing: client.go", func() {

var scheme *runtime.Scheme

BeforeEach(func() {
scheme = runtime.NewScheme()
corev1.AddToScheme(scheme)
})

It("should create a functional client", func() {
clnt, err := NewClientFor(env.Config(), scheme, "test-controller")
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Config()).To(Equal(env.Config()))

resp, err := clnt.HttpClient().Get(fmt.Sprintf("%sversion", env.Config().Host))
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(200))

kubeSystemNamespace := &corev1.Namespace{}
err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, kubeSystemNamespace)
Expect(err).NotTo(HaveOccurred())

version, err := clnt.DiscoveryClient().ServerVersion()
Expect(err).NotTo(HaveOccurred())
Expect(version.String()).To(Equal(env.Version().String()))
clnt.EventRecorder().Event(kubeSystemNamespace, corev1.EventTypeNormal, "TestEvent", "This is a test event")
Eventually(func() error {
eventList := &corev1.EventList{}
selector, err := fields.ParseSelector("involvedObject.apiVersion=v1,involvedObject.kind=Namespace,involvedObject.name=kube-system")
if err != nil {
return err
}
err = clnt.List(context.Background(), eventList, client.InNamespace("default"), client.MatchingFieldsSelector{Selector: selector})
if err != nil {
return err
}
if slices.Any(eventList.Items, func(event corev1.Event) bool {
return event.Source.Component == "test-controller" && event.Reason == "TestEvent" && event.Message == "This is a test event"
}) {
return nil
}
return fmt.Errorf("event not found")
}, 10*time.Second, 1*time.Second).Should(Succeed())
})

})
9 changes: 7 additions & 2 deletions internal/clientfactory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type ClientFactory struct {
name string
controllerName string
config *rest.Config
validity time.Duration
scheme *runtime.Scheme
clients map[string]*Client
}
Expand All @@ -37,6 +38,8 @@ const validity = 15 * time.Minute

func NewClientFactory(name string, controllerName string, config *rest.Config, schemeBuilders []types.SchemeBuilder) (*ClientFactory, error) {
scheme := runtime.NewScheme()
// note: it would be sufficient to just add the corev1 scheme instead of the whole client-go scheme;
// but for convenience, we add all the api groups
if err := clientgoscheme.AddToScheme(scheme); err != nil {
return nil, err
}
Expand All @@ -56,6 +59,7 @@ func NewClientFactory(name string, controllerName string, config *rest.Config, s
name: name,
controllerName: controllerName,
config: config,
validity: validity,
scheme: scheme,
clients: make(map[string]*Client),
}
Expand Down Expand Up @@ -116,15 +120,15 @@ func (f *ClientFactory) Get(kubeConfig []byte, impersonationUser string, imperso
key := sha256sum(keyData)

if clnt, ok := f.clients[key]; ok {
clnt.validUntil = time.Now().Add(validity)
clnt.validUntil = time.Now().Add(f.validity)
return clnt, nil
}

clnt, err := NewClientFor(config, f.scheme, f.name)
if err != nil {
return nil, err
}
clnt.validUntil = time.Now().Add(validity)
clnt.validUntil = time.Now().Add(f.validity)
f.clients[key] = clnt
metrics.CreatedClients.WithLabelValues(f.controllerName).Inc()
metrics.ActiveClients.WithLabelValues(f.controllerName).Set(float64(len(f.clients)))
Expand All @@ -134,6 +138,7 @@ func (f *ClientFactory) Get(kubeConfig []byte, impersonationUser string, imperso
return clnt, nil
}

// TODO: this could be repplaced by util.CalculateDigest()
func sha256sum(data any) string {
dataAsJson, err := json.Marshal(data)
if err != nil {
Expand Down
182 changes: 182 additions & 0 deletions internal/clientfactory/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package clientfactory

import (
"context"
"time"

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"
"k8s.io/apimachinery/pkg/runtime/schema"
apitypes "k8s.io/apimachinery/pkg/types"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/sap/component-operator-runtime/pkg/types"
cstestingv1alpha1 "github.com/sap/component-operator-runtime/testing/environment/apis/testing.cs.sap.com/v1alpha1"
)

var _ = Describe("testing: factory.go", func() {

var factory *ClientFactory
var namespace string

BeforeEach(func() {
var err error

schemeBuilder := runtime.NewSchemeBuilder(cstestingv1alpha1.AddToScheme)

factory, err = NewClientFactory("test-controller", "testing", env.Config(), []types.SchemeBuilder{&schemeBuilder})
Expect(err).NotTo(HaveOccurred())

namespace, err = env.CreateNamespace()
Expect(err).NotTo(HaveOccurred())

err = env.CreateObject(
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace,
},
})
Expect(err).NotTo(HaveOccurred())
})

It("should create a functional client without impersonation", func() {
clnt, err := factory.Get(nil, "", nil)
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).NotTo(HaveOccurred())

clnt, err = factory.Get([]byte{}, "", nil)
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).NotTo(HaveOccurred())

clnt, err = factory.Get([]byte(env.KubeConfig()), "", nil)
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).NotTo(HaveOccurred())
})

It("should create a functional client with impersonation (service account)", func() {
clnt, err := factory.Get(nil, "system:serviceaccount:default:configmap-reader", nil)
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).To(HaveOccurred())
Expect(apierrors.IsForbidden(err)).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Namespace: namespace, Name: "test"}, &corev1.ConfigMap{})
Expect(err).NotTo(HaveOccurred())
})

It("should create a functional client with impersonation (user)", func() {
clnt, err := factory.Get(nil, "configmap-reader", nil)
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).To(HaveOccurred())
Expect(apierrors.IsForbidden(err)).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Namespace: namespace, Name: "test"}, &corev1.ConfigMap{})
Expect(err).NotTo(HaveOccurred())
})

It("should create a functional client with impersonation (group)", func() {
clnt, err := factory.Get(nil, "nobody", []string{"configmap-readers"})
Expect(err).NotTo(HaveOccurred())

Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"})).To(BeTrue())
Expect(clnt.Scheme().Recognizes(schema.GroupVersionKind{Group: "testing.cs.sap.com", Version: "v1alpha1", Kind: "Foo"})).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, &corev1.Namespace{})
Expect(err).To(HaveOccurred())
Expect(apierrors.IsForbidden(err)).To(BeTrue())

err = clnt.Get(context.Background(), apitypes.NamespacedName{Namespace: namespace, Name: "test"}, &corev1.ConfigMap{})
Expect(err).NotTo(HaveOccurred())
})

It("should return cached clients and evict expired clients", func() {
factory.validity = 5 * time.Second

clnt, err := factory.Get(nil, "", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(1))

clnt2, err := factory.Get(nil, "", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(1))
Expect(clnt2).To(BeIdenticalTo(clnt))

clnt, err = factory.Get([]byte(env.KubeConfig()), "", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(2))

clnt2, err = factory.Get([]byte(env.KubeConfig()), "", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(2))
Expect(clnt2).To(BeIdenticalTo(clnt))

clnt, err = factory.Get([]byte(env.KubeConfig()), "user", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(3))

clnt2, err = factory.Get([]byte(env.KubeConfig()), "user", nil)
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(3))
Expect(clnt2).To(BeIdenticalTo(clnt))

clnt, err = factory.Get([]byte(env.KubeConfig()), "user", []string{"group"})
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(4))

clnt2, err = factory.Get([]byte(env.KubeConfig()), "user", []string{"group"})
Expect(err).NotTo(HaveOccurred())
Expect(factory.clients).To(HaveLen(4))
Expect(clnt2).To(BeIdenticalTo(clnt))

Eventually(func() int {
return len(factory.clients)
}, 15*time.Second, 1*time.Second).Should(Equal(0))
})

})
Loading