sigs.k8s.io/cluster-api@v1.7.1/internal/runtime/client/client.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 client provides the Runtime SDK client.
    18  package client
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"path"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/pkg/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/labels"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    40  	utilnet "k8s.io/apimachinery/pkg/util/net"
    41  	"k8s.io/apimachinery/pkg/util/validation"
    42  	"k8s.io/client-go/transport"
    43  	"k8s.io/utils/ptr"
    44  	ctrl "sigs.k8s.io/controller-runtime"
    45  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    46  
    47  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    48  	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
    49  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    50  	runtimemetrics "sigs.k8s.io/cluster-api/internal/runtime/metrics"
    51  	runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
    52  	"sigs.k8s.io/cluster-api/util"
    53  )
    54  
    55  type errCallingExtensionHandler error
    56  
    57  const defaultDiscoveryTimeout = 10 * time.Second
    58  
    59  // Options are creation options for a Client.
    60  type Options struct {
    61  	Catalog  *runtimecatalog.Catalog
    62  	Registry runtimeregistry.ExtensionRegistry
    63  	Client   ctrlclient.Client
    64  }
    65  
    66  // New returns a new Client.
    67  func New(options Options) Client {
    68  	return &client{
    69  		catalog:  options.Catalog,
    70  		registry: options.Registry,
    71  		client:   options.Client,
    72  	}
    73  }
    74  
    75  // Client is the runtime client to interact with extensions.
    76  type Client interface {
    77  	// WarmUp can be used to initialize a "cold" RuntimeClient with all
    78  	// known runtimev1.ExtensionConfigs at a given time.
    79  	// After WarmUp completes the RuntimeClient is considered ready.
    80  	WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error
    81  
    82  	// IsReady return true after the RuntimeClient finishes warmup.
    83  	IsReady() bool
    84  
    85  	// Discover makes the discovery call on the extension and returns an updated ExtensionConfig
    86  	// with extension handlers information in the ExtensionConfig status.
    87  	Discover(context.Context, *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error)
    88  
    89  	// Register registers the ExtensionConfig.
    90  	Register(extensionConfig *runtimev1.ExtensionConfig) error
    91  
    92  	// Unregister unregisters the ExtensionConfig.
    93  	Unregister(extensionConfig *runtimev1.ExtensionConfig) error
    94  
    95  	// CallAllExtensions calls all the ExtensionHandler registered for the hook.
    96  	CallAllExtensions(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error
    97  
    98  	// CallExtension calls the ExtensionHandler with the given name.
    99  	CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error
   100  }
   101  
   102  var _ Client = &client{}
   103  
   104  type client struct {
   105  	catalog  *runtimecatalog.Catalog
   106  	registry runtimeregistry.ExtensionRegistry
   107  	client   ctrlclient.Client
   108  }
   109  
   110  func (c *client) WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error {
   111  	return c.registry.WarmUp(extensionConfigList)
   112  }
   113  
   114  func (c *client) IsReady() bool {
   115  	return c.registry.IsReady()
   116  }
   117  
   118  func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) {
   119  	log := ctrl.LoggerFrom(ctx)
   120  	log.Info("Performing discovery for ExtensionConfig")
   121  
   122  	hookGVH, err := c.catalog.GroupVersionHook(runtimehooksv1.Discovery)
   123  	if err != nil {
   124  		return nil, errors.Wrapf(err, "failed to discover extension %q: failed to compute GVH of hook", extensionConfig.Name)
   125  	}
   126  
   127  	request := &runtimehooksv1.DiscoveryRequest{}
   128  	response := &runtimehooksv1.DiscoveryResponse{}
   129  	opts := &httpCallOptions{
   130  		catalog:         c.catalog,
   131  		config:          extensionConfig.Spec.ClientConfig,
   132  		registrationGVH: hookGVH,
   133  		hookGVH:         hookGVH,
   134  		timeout:         defaultDiscoveryTimeout,
   135  	}
   136  	if err := httpCall(ctx, request, response, opts); err != nil {
   137  		return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
   138  	}
   139  
   140  	// Check to see if the response is a failure and handle the failure accordingly.
   141  	if response.GetStatus() == runtimehooksv1.ResponseStatusFailure {
   142  		log.Info(fmt.Sprintf("failed to discover extension %q: got failure response with message %v", extensionConfig.Name, response.GetMessage()))
   143  		// Don't add the message to the error as it is may be unique causing too many reconciliations. Ref: https://github.com/kubernetes-sigs/cluster-api/issues/6921
   144  		return nil, errors.Errorf("failed to discover extension %q: got failure response", extensionConfig.Name)
   145  	}
   146  
   147  	// Check to see if the response is valid.
   148  	if err = defaultAndValidateDiscoveryResponse(c.catalog, response); err != nil {
   149  		return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
   150  	}
   151  
   152  	modifiedExtensionConfig := extensionConfig.DeepCopy()
   153  	// Reset the handlers that were previously registered with the ExtensionConfig.
   154  	modifiedExtensionConfig.Status.Handlers = []runtimev1.ExtensionHandler{}
   155  
   156  	for _, handler := range response.Handlers {
   157  		handlerName, err := NameForHandler(handler, extensionConfig)
   158  		if err != nil {
   159  			return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
   160  		}
   161  		modifiedExtensionConfig.Status.Handlers = append(
   162  			modifiedExtensionConfig.Status.Handlers,
   163  			runtimev1.ExtensionHandler{
   164  				Name: handlerName, // Uniquely identifies a handler of an Extension.
   165  				RequestHook: runtimev1.GroupVersionHook{
   166  					APIVersion: handler.RequestHook.APIVersion,
   167  					Hook:       handler.RequestHook.Hook,
   168  				},
   169  				TimeoutSeconds: handler.TimeoutSeconds,
   170  				FailurePolicy:  (*runtimev1.FailurePolicy)(handler.FailurePolicy),
   171  			},
   172  		)
   173  	}
   174  
   175  	return modifiedExtensionConfig, nil
   176  }
   177  
   178  func (c *client) Register(extensionConfig *runtimev1.ExtensionConfig) error {
   179  	if err := c.registry.Add(extensionConfig); err != nil {
   180  		return errors.Wrapf(err, "failed to register ExtensionConfig %q", extensionConfig.Name)
   181  	}
   182  	return nil
   183  }
   184  
   185  func (c *client) Unregister(extensionConfig *runtimev1.ExtensionConfig) error {
   186  	if err := c.registry.Remove(extensionConfig); err != nil {
   187  		return errors.Wrapf(err, "failed to unregister ExtensionConfig %q", extensionConfig.Name)
   188  	}
   189  	return nil
   190  }
   191  
   192  // CallAllExtensions calls all the ExtensionHandlers registered for the hook.
   193  // The ExtensionHandlers are called sequentially. The function exits immediately after any of the ExtensionHandlers return an error.
   194  // This ensures we don't end up waiting for timeout from multiple unreachable Extensions.
   195  // See CallExtension for more details on when an ExtensionHandler returns an error.
   196  // The aggregated result of the ExtensionHandlers is updated into the response object passed to the function.
   197  func (c *client) CallAllExtensions(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error {
   198  	hookName := runtimecatalog.HookName(hook)
   199  	log := ctrl.LoggerFrom(ctx).WithValues("hook", hookName)
   200  	ctx = ctrl.LoggerInto(ctx, log)
   201  	gvh, err := c.catalog.GroupVersionHook(hook)
   202  	if err != nil {
   203  		return errors.Wrapf(err, "failed to call extension handlers for hook %q: failed to compute GroupVersionHook", hookName)
   204  	}
   205  	// Make sure the request is compatible with the hook.
   206  	if err := c.catalog.ValidateRequest(gvh, request); err != nil {
   207  		return errors.Wrapf(err, "failed to call extension handlers for hook %q: request object is invalid for hook", gvh.GroupHook())
   208  	}
   209  	// Make sure the response is compatible with the hook.
   210  	if err := c.catalog.ValidateResponse(gvh, response); err != nil {
   211  		return errors.Wrapf(err, "failed to call extension handlers for hook %q: response object is invalid for hook", gvh.GroupHook())
   212  	}
   213  
   214  	registrations, err := c.registry.List(gvh.GroupHook())
   215  	if err != nil {
   216  		return errors.Wrapf(err, "failed to call extension handlers for hook %q", gvh.GroupHook())
   217  	}
   218  
   219  	log.Info(fmt.Sprintf("Calling all extensions of hook %q", hookName))
   220  	responses := []runtimehooksv1.ResponseObject{}
   221  	for _, registration := range registrations {
   222  		// Creates a new instance of the response parameter.
   223  		responseObject, err := c.catalog.NewResponse(gvh)
   224  		if err != nil {
   225  			return errors.Wrapf(err, "failed to call extension handlers for hook %q: failed to call extension handler %q", gvh.GroupHook(), registration.Name)
   226  		}
   227  		tmpResponse := responseObject.(runtimehooksv1.ResponseObject)
   228  
   229  		// Compute whether the object the call is being made for matches the namespaceSelector
   230  		namespaceMatches, err := c.matchNamespace(ctx, registration.NamespaceSelector, forObject.GetNamespace())
   231  		if err != nil {
   232  			return errors.Wrapf(err, "failed to call extension handlers for hook %q: failed to call extension handler %q", gvh.GroupHook(), registration.Name)
   233  		}
   234  		// If the object namespace isn't matched by the registration NamespaceSelector skip the call.
   235  		if !namespaceMatches {
   236  			log.V(5).Info(fmt.Sprintf("skipping extension handler %q as object '%s/%s' does not match selector %q of ExtensionConfig", registration.Name, forObject.GetNamespace(), forObject.GetName(), registration.NamespaceSelector))
   237  			continue
   238  		}
   239  
   240  		err = c.CallExtension(ctx, hook, forObject, registration.Name, request, tmpResponse)
   241  		// If one of the extension handlers fails lets short-circuit here and return early.
   242  		if err != nil {
   243  			log.Error(err, "failed to call extension handlers")
   244  			return errors.Wrapf(err, "failed to call extension handlers for hook %q", gvh.GroupHook())
   245  		}
   246  		responses = append(responses, tmpResponse)
   247  	}
   248  
   249  	// Aggregate all responses into a single response.
   250  	// Note: we only get here if all the extension handlers succeeded.
   251  	aggregateSuccessfulResponses(response, responses)
   252  
   253  	return nil
   254  }
   255  
   256  // aggregateSuccessfulResponses aggregates all successful responses into a single response.
   257  func aggregateSuccessfulResponses(aggregatedResponse runtimehooksv1.ResponseObject, responses []runtimehooksv1.ResponseObject) {
   258  	// At this point the Status should always be ResponseStatusSuccess.
   259  	aggregatedResponse.SetStatus(runtimehooksv1.ResponseStatusSuccess)
   260  
   261  	// Note: As all responses have the same type we can assume now that
   262  	// they all implement the RetryResponseObject interface.
   263  	messages := []string{}
   264  	for _, resp := range responses {
   265  		aggregatedRetryResponse, ok := aggregatedResponse.(runtimehooksv1.RetryResponseObject)
   266  		if ok {
   267  			aggregatedRetryResponse.SetRetryAfterSeconds(util.LowestNonZeroInt32(
   268  				aggregatedRetryResponse.GetRetryAfterSeconds(),
   269  				resp.(runtimehooksv1.RetryResponseObject).GetRetryAfterSeconds(),
   270  			))
   271  		}
   272  		if resp.GetMessage() != "" {
   273  			messages = append(messages, resp.GetMessage())
   274  		}
   275  	}
   276  	aggregatedResponse.SetMessage(strings.Join(messages, ", "))
   277  }
   278  
   279  // CallExtension makes the call to the extension with the given name.
   280  // The response object passed will be updated with the response of the call.
   281  // An error is returned if the extension is not compatible with the hook.
   282  // If the ExtensionHandler returns a response with `Status` set to `Failure` the function returns an error
   283  // and the response object is updated with the response received from the extension handler.
   284  //
   285  // FailurePolicy of the ExtensionHandler is used to handle errors that occur when performing the external call to the extension.
   286  // - If FailurePolicy is set to Ignore, the error is ignored and the response object is updated to be the default success response.
   287  // - If FailurePolicy is set to Fail, an error is returned and the response object may or may not be updated.
   288  // Nb. FailurePolicy does not affect the following kinds of errors:
   289  // - Internal errors. Examples: hooks is incompatible with ExtensionHandler, ExtensionHandler information is missing.
   290  // - Error when ExtensionHandler returns a response with `Status` set to `Failure`.
   291  func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, forObject metav1.Object, name string, request runtimehooksv1.RequestObject, response runtimehooksv1.ResponseObject) error {
   292  	log := ctrl.LoggerFrom(ctx).WithValues("extensionHandler", name, "hook", runtimecatalog.HookName(hook))
   293  	ctx = ctrl.LoggerInto(ctx, log)
   294  	hookGVH, err := c.catalog.GroupVersionHook(hook)
   295  	if err != nil {
   296  		return errors.Wrapf(err, "failed to call extension handler %q: failed to compute GroupVersionHook", name)
   297  	}
   298  	// Make sure the request is compatible with the hook.
   299  	if err := c.catalog.ValidateRequest(hookGVH, request); err != nil {
   300  		return errors.Wrapf(err, "failed to call extension handler %q: request object is invalid for hook %q", name, hookGVH)
   301  	}
   302  	// Make sure the response is compatible with the hook.
   303  	if err := c.catalog.ValidateResponse(hookGVH, response); err != nil {
   304  		return errors.Wrapf(err, "failed to call extension handler %q: response object is invalid for hook %q", name, hookGVH)
   305  	}
   306  
   307  	registration, err := c.registry.Get(name)
   308  	if err != nil {
   309  		return errors.Wrapf(err, "failed to call extension handler %q", name)
   310  	}
   311  	if hookGVH.GroupHook() != registration.GroupVersionHook.GroupHook() {
   312  		return errors.Errorf("failed to call extension handler %q: handler does not match GroupHook %q", name, hookGVH.GroupHook())
   313  	}
   314  
   315  	// Compute whether the object the call is being made for matches the namespaceSelector
   316  	namespaceMatches, err := c.matchNamespace(ctx, registration.NamespaceSelector, forObject.GetNamespace())
   317  	if err != nil {
   318  		return errors.Errorf("failed to call extension handler %q", name)
   319  	}
   320  	// If the object namespace isn't matched by the registration NamespaceSelector return an error.
   321  	if !namespaceMatches {
   322  		return errors.Errorf("failed to call extension handler %q: namespaceSelector did not match object %s", name, util.ObjectKey(forObject))
   323  	}
   324  
   325  	log.Info(fmt.Sprintf("Calling extension handler %q", name))
   326  	timeoutDuration := runtimehooksv1.DefaultHandlersTimeoutSeconds * time.Second
   327  	if registration.TimeoutSeconds != nil {
   328  		timeoutDuration = time.Duration(*registration.TimeoutSeconds) * time.Second
   329  	}
   330  
   331  	// Prepare the request by merging the settings in the registration with the settings in the request.
   332  	request = cloneAndAddSettings(request, registration.Settings)
   333  
   334  	opts := &httpCallOptions{
   335  		catalog:         c.catalog,
   336  		config:          registration.ClientConfig,
   337  		registrationGVH: registration.GroupVersionHook,
   338  		hookGVH:         hookGVH,
   339  		name:            strings.TrimSuffix(registration.Name, "."+registration.ExtensionConfigName),
   340  		timeout:         timeoutDuration,
   341  	}
   342  	err = httpCall(ctx, request, response, opts)
   343  	if err != nil {
   344  		// If the error is errCallingExtensionHandler then apply failure policy to calculate
   345  		// the effective result of the operation.
   346  		ignore := *registration.FailurePolicy == runtimev1.FailurePolicyIgnore
   347  		if _, ok := err.(errCallingExtensionHandler); ok && ignore {
   348  			// Update the response to a default success response and return.
   349  			log.Info(fmt.Sprintf("ignoring error calling extension handler because of FailurePolicy %q", *registration.FailurePolicy))
   350  			response.SetStatus(runtimehooksv1.ResponseStatusSuccess)
   351  			response.SetMessage("")
   352  			return nil
   353  		}
   354  		log.Error(err, "failed to call extension handler")
   355  		return errors.Wrapf(err, "failed to call extension handler %q", name)
   356  	}
   357  
   358  	// If the received response is a failure then return an error.
   359  	if response.GetStatus() == runtimehooksv1.ResponseStatusFailure {
   360  		log.Info(fmt.Sprintf("failed to call extension handler %q: got failure response with message %v", name, response.GetMessage()))
   361  		// Don't add the message to the error as it is may be unique causing too many reconciliations. Ref: https://github.com/kubernetes-sigs/cluster-api/issues/6921
   362  		return errors.Errorf("failed to call extension handler %q: got failure response", name)
   363  	}
   364  
   365  	if retryResponse, ok := response.(runtimehooksv1.RetryResponseObject); ok && retryResponse.GetRetryAfterSeconds() != 0 {
   366  		log.Info(fmt.Sprintf("extension handler returned blocking response with retryAfterSeconds of %d", retryResponse.GetRetryAfterSeconds()))
   367  	} else {
   368  		log.Info("extension handler returned success response")
   369  	}
   370  
   371  	// Received a successful response from the extension handler. The `response` object
   372  	// has been populated with the result. Return no error.
   373  	return nil
   374  }
   375  
   376  // cloneAndAddSettings creates a new request object and adds settings to it.
   377  func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSettings map[string]string) runtimehooksv1.RequestObject {
   378  	// Merge the settings from registration with the settings in the request.
   379  	// The values in request take precedence over the values in the registration.
   380  	// Create a deepcopy object to avoid side-effects on the request object.
   381  	request = request.DeepCopyObject().(runtimehooksv1.RequestObject)
   382  	settings := map[string]string{}
   383  	for k, v := range registrationSettings {
   384  		settings[k] = v
   385  	}
   386  	for k, v := range request.GetSettings() {
   387  		settings[k] = v
   388  	}
   389  	request.SetSettings(settings)
   390  	return request
   391  }
   392  
   393  type httpCallOptions struct {
   394  	catalog         *runtimecatalog.Catalog
   395  	config          runtimev1.ClientConfig
   396  	registrationGVH runtimecatalog.GroupVersionHook
   397  	hookGVH         runtimecatalog.GroupVersionHook
   398  	name            string
   399  	timeout         time.Duration
   400  }
   401  
   402  func httpCall(ctx context.Context, request, response runtime.Object, opts *httpCallOptions) error {
   403  	log := ctrl.LoggerFrom(ctx)
   404  	if opts == nil || request == nil || response == nil {
   405  		return errors.New("http call failed: opts, request and response cannot be nil")
   406  	}
   407  	if opts.catalog == nil {
   408  		return errors.New("http call failed: opts.Catalog cannot be nil")
   409  	}
   410  
   411  	extensionURL, err := urlForExtension(opts.config, opts.registrationGVH, opts.name)
   412  	if err != nil {
   413  		return errors.Wrap(err, "http call failed")
   414  	}
   415  
   416  	// Observe request duration metric.
   417  	start := time.Now()
   418  	defer func() {
   419  		runtimemetrics.RequestDuration.Observe(opts.hookGVH, *extensionURL, time.Since(start))
   420  	}()
   421  	requireConversion := opts.registrationGVH.Version != opts.hookGVH.Version
   422  
   423  	requestLocal := request
   424  	responseLocal := response
   425  
   426  	if requireConversion {
   427  		log.V(5).Info(fmt.Sprintf("Hook version of supported request is %s. Converting request from %s", opts.registrationGVH, opts.hookGVH))
   428  		// The request and response objects need to be converted to match the version supported by
   429  		// the ExtensionHandler.
   430  		var err error
   431  
   432  		// Create a new hook request object that is compatible with the version of ExtensionHandler.
   433  		requestLocal, err = opts.catalog.NewRequest(opts.registrationGVH)
   434  		if err != nil {
   435  			return errors.Wrap(err, "http call failed")
   436  		}
   437  
   438  		// Convert the request to the version supported by the ExtensionHandler.
   439  		if err := opts.catalog.Convert(request, requestLocal, ctx); err != nil {
   440  			return errors.Wrapf(err, "http call failed: failed to convert request from %T to %T", request, requestLocal)
   441  		}
   442  
   443  		// Create a new hook response object that is compatible with the version of the ExtensionHandler.
   444  		responseLocal, err = opts.catalog.NewResponse(opts.registrationGVH)
   445  		if err != nil {
   446  			return errors.Wrap(err, "http call failed")
   447  		}
   448  	}
   449  
   450  	// Ensure the GroupVersionKind is set to the request.
   451  	requestGVH, err := opts.catalog.Request(opts.registrationGVH)
   452  	if err != nil {
   453  		return errors.Wrap(err, "http call failed")
   454  	}
   455  	requestLocal.GetObjectKind().SetGroupVersionKind(requestGVH)
   456  
   457  	postBody, err := json.Marshal(requestLocal)
   458  	if err != nil {
   459  		return errors.Wrap(err, "http call failed: failed to marshall request object")
   460  	}
   461  
   462  	if opts.timeout != 0 {
   463  		// Make the call time-bound if timeout is non-zero value.
   464  		values := extensionURL.Query()
   465  		values.Add("timeout", opts.timeout.String())
   466  		extensionURL.RawQuery = values.Encode()
   467  
   468  		var cancel context.CancelFunc
   469  		ctx, cancel = context.WithTimeout(ctx, opts.timeout)
   470  		defer cancel()
   471  	}
   472  
   473  	httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, extensionURL.String(), bytes.NewBuffer(postBody))
   474  	if err != nil {
   475  		return errors.Wrap(err, "http call failed: failed to create http request")
   476  	}
   477  
   478  	// Use client-go's transport.TLSConfigureFor to ensure good defaults for tls
   479  	client := http.DefaultClient
   480  	tlsConfig, err := transport.TLSConfigFor(&transport.Config{
   481  		TLS: transport.TLSConfig{
   482  			CAData:     opts.config.CABundle,
   483  			ServerName: extensionURL.Hostname(),
   484  		},
   485  	})
   486  	if err != nil {
   487  		return errors.Wrap(err, "http call failed: failed to create tls config")
   488  	}
   489  	// This also adds http2
   490  	client.Transport = utilnet.SetTransportDefaults(&http.Transport{
   491  		TLSClientConfig: tlsConfig,
   492  	})
   493  
   494  	resp, err := client.Do(httpRequest)
   495  
   496  	// Create http request metric.
   497  	defer func() {
   498  		runtimemetrics.RequestsTotal.Observe(httpRequest, resp, opts.hookGVH, err, response)
   499  	}()
   500  
   501  	if err != nil {
   502  		return errCallingExtensionHandler(
   503  			errors.Wrapf(err, "http call failed"),
   504  		)
   505  	}
   506  	defer resp.Body.Close()
   507  
   508  	if resp.StatusCode != http.StatusOK {
   509  		respBody, err := io.ReadAll(resp.Body)
   510  		if err != nil {
   511  			return errCallingExtensionHandler(
   512  				errors.Errorf("http call failed: got response with status code %d != 200: failed to read response body", resp.StatusCode),
   513  			)
   514  		}
   515  
   516  		return errCallingExtensionHandler(
   517  			errors.Errorf("http call failed: got response with status code %d != 200: response: %q", resp.StatusCode, string(respBody)),
   518  		)
   519  	}
   520  
   521  	if err := json.NewDecoder(resp.Body).Decode(responseLocal); err != nil {
   522  		return errCallingExtensionHandler(
   523  			errors.Wrap(err, "http call failed: failed to decode response"),
   524  		)
   525  	}
   526  
   527  	if requireConversion {
   528  		log.V(5).Info(fmt.Sprintf("Hook version of received response is %s. Converting response to %s", opts.registrationGVH, opts.hookGVH))
   529  		// Convert the received response to the original version of the response object.
   530  		if err := opts.catalog.Convert(responseLocal, response, ctx); err != nil {
   531  			return errors.Wrapf(err, "http call failed: failed to convert response from %T to %T", requestLocal, response)
   532  		}
   533  	}
   534  
   535  	return nil
   536  }
   537  
   538  func urlForExtension(config runtimev1.ClientConfig, gvh runtimecatalog.GroupVersionHook, name string) (*url.URL, error) {
   539  	var u *url.URL
   540  	if config.Service != nil {
   541  		// The Extension's ClientConfig points ot a service. Construct the URL to the service.
   542  		svc := config.Service
   543  		host := svc.Name + "." + svc.Namespace + ".svc"
   544  		if svc.Port != nil {
   545  			host = net.JoinHostPort(host, strconv.Itoa(int(*svc.Port)))
   546  		}
   547  		u = &url.URL{
   548  			Scheme: "https",
   549  			Host:   host,
   550  		}
   551  		if svc.Path != nil {
   552  			u.Path = *svc.Path
   553  		}
   554  	} else {
   555  		if config.URL == nil {
   556  			return nil, errors.New("failed to compute URL: at least one of service and url should be defined in config")
   557  		}
   558  
   559  		var err error
   560  		u, err = url.Parse(*config.URL)
   561  		if err != nil {
   562  			return nil, errors.Wrap(err, "failed to compute URL: failed to parse url from clientConfig")
   563  		}
   564  
   565  		if u.Scheme != "https" {
   566  			return nil, errors.Errorf("failed to compute URL: expected https scheme, got %s", u.Scheme)
   567  		}
   568  	}
   569  
   570  	// Append the ExtensionHandler path.
   571  	u.Path = path.Join(u.Path, runtimecatalog.GVHToPath(gvh, name))
   572  	return u, nil
   573  }
   574  
   575  // defaultAndValidateDiscoveryResponse defaults unset values and runs a set of validations on the Discovery Response.
   576  // If any of these checks fails the response is invalid and an error is returned.
   577  func defaultAndValidateDiscoveryResponse(cat *runtimecatalog.Catalog, discovery *runtimehooksv1.DiscoveryResponse) error {
   578  	if discovery == nil {
   579  		return errors.New("failed to validate discovery response: response is nil")
   580  	}
   581  
   582  	discovery = defaultDiscoveryResponse(discovery)
   583  
   584  	var errs []error
   585  	names := make(map[string]bool)
   586  	for _, handler := range discovery.Handlers {
   587  		// Names should be unique.
   588  		if _, ok := names[handler.Name]; ok {
   589  			errs = append(errs, errors.Errorf("duplicate name for handler %s found", handler.Name))
   590  		}
   591  		names[handler.Name] = true
   592  
   593  		// Name should match Kubernetes naming conventions - validated based on DNS1123 label rules.
   594  		if errStrings := validation.IsDNS1123Label(handler.Name); len(errStrings) > 0 {
   595  			errs = append(errs, errors.Errorf("handler name %s is not valid: %s", handler.Name, errStrings))
   596  		}
   597  
   598  		// Timeout should be a positive integer not greater than 30.
   599  		if *handler.TimeoutSeconds < 0 || *handler.TimeoutSeconds > 30 {
   600  			errs = append(errs, errors.Errorf("handler %s timeoutSeconds %d must be between 0 and 30", handler.Name, *handler.TimeoutSeconds))
   601  		}
   602  
   603  		// FailurePolicy must be one of Ignore or Fail.
   604  		if *handler.FailurePolicy != runtimehooksv1.FailurePolicyFail && *handler.FailurePolicy != runtimehooksv1.FailurePolicyIgnore {
   605  			errs = append(errs, errors.Errorf("handler %s failurePolicy %s must equal \"Ignore\" or \"Fail\"", handler.Name, *handler.FailurePolicy))
   606  		}
   607  
   608  		gv, err := schema.ParseGroupVersion(handler.RequestHook.APIVersion)
   609  		if err != nil {
   610  			errs = append(errs, errors.Wrapf(err, "handler %s requestHook APIVersion %s is not valid", handler.Name, handler.RequestHook.APIVersion))
   611  		} else if !cat.IsHookRegistered(runtimecatalog.GroupVersionHook{
   612  			Group:   gv.Group,
   613  			Version: gv.Version,
   614  			Hook:    handler.RequestHook.Hook,
   615  		}) {
   616  			errs = append(errs, errors.Errorf("handler %s requestHook %s/%s is not in the Runtime SDK catalog", handler.Name, handler.RequestHook.APIVersion, handler.RequestHook.Hook))
   617  		}
   618  	}
   619  
   620  	return errors.Wrapf(kerrors.NewAggregate(errs), "failed to validate discovery response")
   621  }
   622  
   623  // defaultDiscoveryResponse defaults FailurePolicy and TimeoutSeconds for all discovered handlers.
   624  func defaultDiscoveryResponse(discovery *runtimehooksv1.DiscoveryResponse) *runtimehooksv1.DiscoveryResponse {
   625  	for i, handler := range discovery.Handlers {
   626  		// If FailurePolicy is not defined set to "Fail".
   627  		if handler.FailurePolicy == nil {
   628  			defaultFailPolicy := runtimehooksv1.FailurePolicyFail
   629  			handler.FailurePolicy = &defaultFailPolicy
   630  		}
   631  
   632  		// If TimeoutSeconds is not defined set to 10.
   633  		if handler.TimeoutSeconds == nil {
   634  			handler.TimeoutSeconds = ptr.To[int32](runtimehooksv1.DefaultHandlersTimeoutSeconds)
   635  		}
   636  
   637  		discovery.Handlers[i] = handler
   638  	}
   639  	return discovery
   640  }
   641  
   642  // matchNamespace returns true if the passed namespace matches the selector. It returns an error if the namespace does
   643  // not exist in the API server.
   644  func (c *client) matchNamespace(ctx context.Context, selector labels.Selector, namespace string) (bool, error) {
   645  	// Return early if the selector is empty.
   646  	if selector.Empty() {
   647  		return true, nil
   648  	}
   649  
   650  	ns := &metav1.PartialObjectMetadata{}
   651  	ns.SetGroupVersionKind(schema.GroupVersionKind{
   652  		Group:   "",
   653  		Version: "v1",
   654  		Kind:    "Namespace",
   655  	})
   656  	if err := c.client.Get(ctx, ctrlclient.ObjectKey{Name: namespace}, ns); err != nil {
   657  		return false, errors.Wrapf(err, "failed to match namespace: failed to get namespace %s", namespace)
   658  	}
   659  
   660  	return selector.Matches(labels.Set(ns.GetLabels())), nil
   661  }
   662  
   663  // NameForHandler constructs a canonical name for a registered runtime extension handler.
   664  func NameForHandler(handler runtimehooksv1.ExtensionHandler, extensionConfig *runtimev1.ExtensionConfig) (string, error) {
   665  	if extensionConfig == nil {
   666  		return "", errors.New("extensionConfig was nil")
   667  	}
   668  	return handler.Name + "." + extensionConfig.Name, nil
   669  }
   670  
   671  // ExtensionNameFromHandlerName extracts the extension name from the canonical name of a registered runtime extension handler.
   672  func ExtensionNameFromHandlerName(registeredHandlerName string) (string, error) {
   673  	parts := strings.Split(registeredHandlerName, ".")
   674  	if len(parts) != 2 {
   675  		return "", errors.Errorf("registered handler name %s was not in the expected format (`HANDLER_NAME.EXTENSION_NAME)", registeredHandlerName)
   676  	}
   677  	return parts[1], nil
   678  }