sigs.k8s.io/cluster-api@v1.7.1/internal/runtime/registry/registry.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package registry
    18  
    19  import (
    20  	"sync"
    21  
    22  	"github.com/pkg/errors"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/labels"
    25  	"k8s.io/apimachinery/pkg/runtime/schema"
    26  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    27  
    28  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    29  	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
    30  )
    31  
    32  // ExtensionRegistry defines the funcs of a RuntimeExtension registry.
    33  type ExtensionRegistry interface {
    34  	// WarmUp can be used to initialize a "cold" RuntimeExtension registry with all
    35  	// known runtimev1.ExtensionConfigs at a given time.
    36  	// After WarmUp completes the RuntimeExtension registry is considered ready.
    37  	WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error
    38  
    39  	// IsReady returns true if the RuntimeExtension registry is ready for usage.
    40  	// This happens after WarmUp is completed.
    41  	IsReady() bool
    42  
    43  	// Add adds all RuntimeExtensions of the given ExtensionConfig.
    44  	// Please note that if the ExtensionConfig has been added before, the
    45  	// corresponding registry entries will get updated/replaced with the
    46  	// one from the newly provided ExtensionConfig.
    47  	Add(extensionConfig *runtimev1.ExtensionConfig) error
    48  
    49  	// Remove removes all RuntimeExtensions corresponding to the provided ExtensionConfig.
    50  	Remove(extensionConfig *runtimev1.ExtensionConfig) error
    51  
    52  	// List lists all registered RuntimeExtensions for a given catalog.GroupHook.
    53  	List(gh runtimecatalog.GroupHook) ([]*ExtensionRegistration, error)
    54  
    55  	// Get gets the RuntimeExtensions with the given name.
    56  	Get(name string) (*ExtensionRegistration, error)
    57  }
    58  
    59  // ExtensionRegistration contains information about a registered RuntimeExtension.
    60  type ExtensionRegistration struct {
    61  	// Name is the unique name of the RuntimeExtension.
    62  	Name string
    63  
    64  	// ExtensionConfigName is the name of the corresponding ExtensionConfig.
    65  	ExtensionConfigName string
    66  
    67  	// GroupVersionHook is the GroupVersionHook that the RuntimeExtension implements.
    68  	GroupVersionHook runtimecatalog.GroupVersionHook
    69  
    70  	// NamespaceSelector limits the objects by namespace for which a Runtime Extension is called.
    71  	NamespaceSelector labels.Selector
    72  
    73  	// ClientConfig is the ClientConfig to communicate with the RuntimeExtension.
    74  	ClientConfig runtimev1.ClientConfig
    75  
    76  	// TimeoutSeconds is the timeout duration used for calls to the RuntimeExtension.
    77  	TimeoutSeconds *int32
    78  
    79  	// FailurePolicy defines how failures in calls to the RuntimeExtension should be handled by a client.
    80  	FailurePolicy *runtimev1.FailurePolicy
    81  
    82  	// Settings captures additional information sent in call to the RuntimeExtensions.
    83  	Settings map[string]string
    84  }
    85  
    86  // extensionRegistry is an implementation of ExtensionRegistry.
    87  type extensionRegistry struct {
    88  	// ready represents if the registry has been warmed up.
    89  	ready bool
    90  	// items contains the registry entries.
    91  	items map[string]*ExtensionRegistration
    92  	// lock is used to synchronize access to fields of the extensionRegistry.
    93  	lock sync.RWMutex
    94  }
    95  
    96  // New returns a new ExtensionRegistry.
    97  func New() ExtensionRegistry {
    98  	return &extensionRegistry{
    99  		items: map[string]*ExtensionRegistration{},
   100  	}
   101  }
   102  
   103  // WarmUp can be used to initialize a "cold" RuntimeExtension registry with all
   104  // known runtimev1.ExtensionConfigs at a given time.
   105  // After WarmUp completes the RuntimeExtension registry is considered ready.
   106  func (r *extensionRegistry) WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error {
   107  	if extensionConfigList == nil {
   108  		return errors.New("failed to warm up registry: invalid argument: when calling WarmUp ExtensionConfigList must not be nil")
   109  	}
   110  
   111  	r.lock.Lock()
   112  	defer r.lock.Unlock()
   113  
   114  	if r.ready {
   115  		return errors.New("failed to warm up registry: invalid operation: WarmUp cannot be called on a registry which has already been warmed up")
   116  	}
   117  
   118  	var allErrs []error
   119  	for i := range extensionConfigList.Items {
   120  		if err := r.add(&extensionConfigList.Items[i]); err != nil {
   121  			allErrs = append(allErrs, err)
   122  		}
   123  	}
   124  	if len(allErrs) > 0 {
   125  		// Reset the map, so that the next WarmUp can start with an empty map
   126  		// and doesn't inherit entries from this failed WarmUp.
   127  		r.items = map[string]*ExtensionRegistration{}
   128  		return errors.Wrapf(kerrors.NewAggregate(allErrs), "failed to warm up registry")
   129  	}
   130  
   131  	r.ready = true
   132  	return nil
   133  }
   134  
   135  // IsReady returns true if the RuntimeExtension registry is ready for usage.
   136  // This happens after WarmUp is completed.
   137  func (r *extensionRegistry) IsReady() bool {
   138  	r.lock.RLock()
   139  	defer r.lock.RUnlock()
   140  
   141  	return r.ready
   142  }
   143  
   144  // Add adds all RuntimeExtensions of the given ExtensionConfig.
   145  // Please note that if the ExtensionConfig has been added before, the
   146  // corresponding registry entries will get updated/replaced with the
   147  // one from the newly provided ExtensionConfig.
   148  func (r *extensionRegistry) Add(extensionConfig *runtimev1.ExtensionConfig) error {
   149  	if extensionConfig == nil {
   150  		return errors.New("failed to add ExtensionConfig to registry: invalid argument: when calling Add extensionConfig must not be nil")
   151  	}
   152  
   153  	r.lock.Lock()
   154  	defer r.lock.Unlock()
   155  
   156  	if !r.ready {
   157  		return errors.Errorf("failed to add ExtensionConfig %q to registry: invalid operation: Add cannot be called on a registry which has not been warmed up", extensionConfig.Name)
   158  	}
   159  
   160  	return r.add(extensionConfig)
   161  }
   162  
   163  // Remove removes all RuntimeExtensions corresponding to the provided ExtensionConfig.
   164  func (r *extensionRegistry) Remove(extensionConfig *runtimev1.ExtensionConfig) error {
   165  	if extensionConfig == nil {
   166  		return errors.New("failed to remove ExtensionConfig from registry: invalid argument: when calling Remove ExtensionConfig must not be nil")
   167  	}
   168  
   169  	r.lock.Lock()
   170  	defer r.lock.Unlock()
   171  
   172  	if !r.ready {
   173  		return errors.Errorf("failed to remove ExtensionConfig %q from registry: invalid operation: Remove cannot be called on a registry which has not been warmed up", extensionConfig.Name)
   174  	}
   175  
   176  	r.remove(extensionConfig)
   177  	return nil
   178  }
   179  
   180  func (r *extensionRegistry) remove(extensionConfig *runtimev1.ExtensionConfig) {
   181  	for _, e := range r.items {
   182  		if e.ExtensionConfigName == extensionConfig.Name {
   183  			delete(r.items, e.Name)
   184  		}
   185  	}
   186  }
   187  
   188  // List lists all registered RuntimeExtensions for a given catalog.GroupHook.
   189  func (r *extensionRegistry) List(gh runtimecatalog.GroupHook) ([]*ExtensionRegistration, error) {
   190  	if gh.Group == "" {
   191  		return nil, errors.New("failed to list extension handlers: invalid argument: when calling List gh.Group must not be empty")
   192  	}
   193  	if gh.Hook == "" {
   194  		return nil, errors.New("failed to list extension handlers: invalid argument: when calling List gh.Hook must not be empty")
   195  	}
   196  
   197  	r.lock.RLock()
   198  	defer r.lock.RUnlock()
   199  
   200  	if !r.ready {
   201  		return nil, errors.Errorf("failed to list extension handlers for GroupHook %q: invalid operation: List cannot be called on a registry which has not been warmed up", gh.String())
   202  	}
   203  
   204  	l := []*ExtensionRegistration{}
   205  	for _, registration := range r.items {
   206  		if registration.GroupVersionHook.Group == gh.Group && registration.GroupVersionHook.Hook == gh.Hook {
   207  			l = append(l, registration)
   208  		}
   209  	}
   210  	return l, nil
   211  }
   212  
   213  // Get gets the RuntimeExtensions with the given name.
   214  func (r *extensionRegistry) Get(name string) (*ExtensionRegistration, error) {
   215  	r.lock.RLock()
   216  	defer r.lock.RUnlock()
   217  
   218  	if !r.ready {
   219  		return nil, errors.Errorf("failed to get extension handler %q from registry: invalid operation: Get cannot be called on a registry not yet ready", name)
   220  	}
   221  
   222  	registration, ok := r.items[name]
   223  	if !ok {
   224  		return nil, errors.Errorf("failed to get extension handler %q from registry: handler with name %q has not been registered", name, name)
   225  	}
   226  
   227  	return registration, nil
   228  }
   229  
   230  func (r *extensionRegistry) add(extensionConfig *runtimev1.ExtensionConfig) error {
   231  	r.remove(extensionConfig)
   232  
   233  	// Create a selector from the NamespaceSelector defined in the extensionConfig spec.
   234  	selector, err := metav1.LabelSelectorAsSelector(extensionConfig.Spec.NamespaceSelector)
   235  	if err != nil {
   236  		return errors.Wrapf(err, "failed to add ExtensionConfig %q to registry: failed to create namespaceSelector", extensionConfig.Name)
   237  	}
   238  
   239  	var allErrs []error
   240  	registrations := []*ExtensionRegistration{}
   241  	for _, e := range extensionConfig.Status.Handlers {
   242  		gv, err := schema.ParseGroupVersion(e.RequestHook.APIVersion)
   243  		if err != nil {
   244  			allErrs = append(allErrs, errors.Wrapf(err, "failed to add extension handler %q to registry: failed to parse GroupVersion %q of handler %q", e.Name, e.RequestHook.APIVersion, e.Name))
   245  			continue
   246  		}
   247  
   248  		// Registrations will only be added to the registry if no errors occur (all or nothing).
   249  		registrations = append(registrations, &ExtensionRegistration{
   250  			ExtensionConfigName: extensionConfig.Name,
   251  			Name:                e.Name,
   252  			GroupVersionHook: runtimecatalog.GroupVersionHook{
   253  				Group:   gv.Group,
   254  				Version: gv.Version,
   255  				Hook:    e.RequestHook.Hook,
   256  			},
   257  			NamespaceSelector: selector,
   258  			ClientConfig:      extensionConfig.Spec.ClientConfig,
   259  			TimeoutSeconds:    e.TimeoutSeconds,
   260  			FailurePolicy:     e.FailurePolicy,
   261  			Settings:          extensionConfig.Spec.Settings,
   262  		})
   263  	}
   264  
   265  	if len(allErrs) > 0 {
   266  		return errors.Wrapf(kerrors.NewAggregate(allErrs), "failed to add ExtensionConfig %q to registry", extensionConfig.Name)
   267  	}
   268  
   269  	for _, registration := range registrations {
   270  		r.items[registration.Name] = registration
   271  	}
   272  
   273  	return nil
   274  }