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 }