Skip to content
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

Optimize list operations for space scoped resrouces #3673

Merged
merged 3 commits into from
Dec 20, 2024
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
99 changes: 99 additions & 0 deletions api/authorization/space_filtering_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package authorization

import (
"context"
"maps"
"slices"

korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func NewSpaceFilteringClient(
client client.WithWatch,
privilegedClient client.WithWatch,
nsPerms *NamespacePermissions,
) SpaceFilteringClient {
return SpaceFilteringClient{
WithWatch: client,
privilegedClient: privilegedClient,
nsPerms: nsPerms,
}
}

type SpaceFilteringClient struct {
client.WithWatch
nsPerms *NamespacePermissions
privilegedClient client.WithWatch
}

func (c SpaceFilteringClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
effectiveListOpts := &client.ListOptions{}
for _, o := range opts {
o.ApplyToList(effectiveListOpts)
}

if effectiveListOpts.Namespace != "" {
return c.WithWatch.List(ctx, list, effectiveListOpts)
}

selector, err := c.buildLabelSelector(ctx, effectiveListOpts)
if err != nil {
return err
}

effectiveListOpts.LabelSelector = selector

return c.privilegedClient.List(ctx, list, effectiveListOpts)
}

func (c SpaceFilteringClient) buildLabelSelector(ctx context.Context, listOpts *client.ListOptions) (labels.Selector, error) {
namespaces, err := c.getAuthorizedSpaceNamespaces(ctx)
if err != nil {
return nil, err
}

if len(namespaces) == 0 {
return matchNotingSelector()
}

selector := labels.NewSelector()
namespaceRequirement, err := labels.NewRequirement(korifiv1alpha1.SpaceGUIDKey, selection.In, namespaces)
if err != nil {
return nil, err
}
selector = selector.Add(*namespaceRequirement)

if listOpts.LabelSelector != nil {
userRequirements, _ := listOpts.LabelSelector.Requirements()
selector = selector.Add(userRequirements...)
}

return selector, nil
}

func matchNotingSelector() (labels.Selector, error) {
r1, err := labels.NewRequirement(korifiv1alpha1.SpaceGUIDKey, selection.Exists, []string{})
if err != nil {
return nil, err
}

r2, err := labels.NewRequirement(korifiv1alpha1.SpaceGUIDKey, selection.DoesNotExist, []string{})
if err != nil {
return nil, err
}

return labels.NewSelector().Add(*r1, *r2), nil
}

func (c SpaceFilteringClient) getAuthorizedSpaceNamespaces(ctx context.Context) ([]string, error) {
authInfo, _ := InfoFromContext(ctx)
authNs, err := c.nsPerms.GetAuthorizedSpaceNamespaces(ctx, authInfo)
if err != nil {
return nil, err
}

return slices.Collect(maps.Keys(authNs)), nil
}
81 changes: 18 additions & 63 deletions api/authorization/user_client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,38 @@ import (
"fmt"
"strings"

k8sclient "k8s.io/client-go/kubernetes"

apierrors "code.cloudfoundry.org/korifi/api/errors"
"code.cloudfoundry.org/korifi/controllers/webhooks/validation"
"code.cloudfoundry.org/korifi/tools/k8s"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type UserK8sClientFactory interface {
type ClientWrappingFunc func(client.WithWatch) client.WithWatch

type UserClientFactory interface {
BuildClient(Info) (client.WithWatch, error)
BuildK8sClient(info Info) (k8sclient.Interface, error)
}

type UnprivilegedClientFactory struct {
config *rest.Config
mapper meta.RESTMapper
backoff wait.Backoff
config *rest.Config
mapper meta.RESTMapper
wrappers []ClientWrappingFunc
}

func NewUnprivilegedClientFactory(config *rest.Config, mapper meta.RESTMapper, backoff wait.Backoff) UnprivilegedClientFactory {
func NewUnprivilegedClientFactory(config *rest.Config, mapper meta.RESTMapper) UnprivilegedClientFactory {
return UnprivilegedClientFactory{
config: rest.AnonymousClientConfig(rest.CopyConfig(config)),
mapper: mapper,
backoff: backoff,
config: rest.AnonymousClientConfig(rest.CopyConfig(config)),
mapper: mapper,
wrappers: []ClientWrappingFunc{},
}
}

func (f UnprivilegedClientFactory) WithWrappingFunc(wrapper ClientWrappingFunc) UnprivilegedClientFactory {
f.wrappers = append(f.wrappers, wrapper)
return f
}

func (f UnprivilegedClientFactory) BuildClient(authInfo Info) (client.WithWatch, error) {
config := rest.CopyConfig(f.config)

Expand Down Expand Up @@ -71,54 +71,9 @@ func (f UnprivilegedClientFactory) BuildClient(authInfo Info) (client.WithWatch,
return nil, apierrors.FromK8sError(err, "")
}

return k8s.NewRetryingClient(userClient, isForbidden, f.backoff), nil
}

// isForbidden returns true for forbidden errors that are NOT korifi webhook
// validation errors, false otherwise upon webhook validation errors it makes
// no sense to retry the operation as the webhook is expected to consistently
// return the same validation error
func isForbidden(err error) bool {
if !k8serrors.IsForbidden(err) {
return false
}

if _, isValidationErr := validation.WebhookErrorToValidationError(err); isValidationErr {
return false
}

return true
}

func (f UnprivilegedClientFactory) BuildK8sClient(authInfo Info) (k8sclient.Interface, error) {
config := rest.CopyConfig(f.config)

switch strings.ToLower(authInfo.Scheme()) {
case BearerScheme:
config.BearerToken = authInfo.Token

case CertScheme:
certBlock, rst := pem.Decode(authInfo.CertData)
if certBlock == nil {
return nil, fmt.Errorf("failed to decode cert PEM")
}

keyBlock, _ := pem.Decode(rst)
if keyBlock == nil {
return nil, fmt.Errorf("failed to decode key PEM")
}

config.CertData = pem.EncodeToMemory(certBlock)
config.KeyData = pem.EncodeToMemory(keyBlock)

default:
return nil, apierrors.NewNotAuthenticatedError(errors.New("unsupported Authorization header scheme"))
}

userK8sClient, err := k8sclient.NewForConfig(config)
if err != nil {
return nil, apierrors.FromK8sError(err, "")
for _, wrapper := range f.wrappers {
userClient = wrapper(userClient)
}

return userK8sClient, nil
return userClient, nil
}
8 changes: 1 addition & 7 deletions api/authorization/user_client_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package authorization_test
import (
"context"
"sync"
"time"

"code.cloudfoundry.org/korifi/api/authorization"
"code.cloudfoundry.org/korifi/api/authorization/testhelpers"
Expand All @@ -18,7 +17,6 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
Expand All @@ -42,11 +40,7 @@ var _ = Describe("Unprivileged User Client Factory", func() {
Expect(err).NotTo(HaveOccurred())
mapper, err := apiutil.NewDynamicRESTMapper(k8sConfig, httpClient)
Expect(err).NotTo(HaveOccurred())
clientFactory = authorization.NewUnprivilegedClientFactory(k8sConfig, mapper, wait.Backoff{
Steps: 6,
Duration: 5 * time.Millisecond,
Factor: 2.0,
})
clientFactory = authorization.NewUnprivilegedClientFactory(k8sConfig, mapper)
})

JustBeforeEach(func() {
Expand Down
60 changes: 60 additions & 0 deletions api/authorization/user_clientset_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package authorization

import (
"encoding/pem"
"errors"
"fmt"
"strings"

k8sclient "k8s.io/client-go/kubernetes"

apierrors "code.cloudfoundry.org/korifi/api/errors"
"k8s.io/client-go/rest"
)

type UserClientsetFactory interface {
BuildClientset(Info) (k8sclient.Interface, error)
}

type UnprivilegedClientsetFactory struct {
config *rest.Config
}

func NewUnprivilegedClientsetFactory(config *rest.Config) UnprivilegedClientsetFactory {
return UnprivilegedClientsetFactory{
config: rest.AnonymousClientConfig(rest.CopyConfig(config)),
}
}

func (f UnprivilegedClientsetFactory) BuildClientset(authInfo Info) (k8sclient.Interface, error) {
config := rest.CopyConfig(f.config)

switch strings.ToLower(authInfo.Scheme()) {
case BearerScheme:
config.BearerToken = authInfo.Token

case CertScheme:
certBlock, rst := pem.Decode(authInfo.CertData)
if certBlock == nil {
return nil, fmt.Errorf("failed to decode cert PEM")
}

keyBlock, _ := pem.Decode(rst)
if keyBlock == nil {
return nil, fmt.Errorf("failed to decode key PEM")
}

config.CertData = pem.EncodeToMemory(certBlock)
config.KeyData = pem.EncodeToMemory(keyBlock)

default:
return nil, apierrors.NewNotAuthenticatedError(errors.New("unsupported Authorization header scheme"))
}

userK8sClient, err := k8sclient.NewForConfig(config)
if err != nil {
return nil, apierrors.FromK8sError(err, "")
}

return userK8sClient, nil
}
Loading
Loading