knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/injection/sharedmain/main.go (about) 1 /* 2 Copyright 2019 The Knative 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 sharedmain 18 19 import ( 20 "context" 21 "errors" 22 "flag" 23 "fmt" 24 "log" 25 "net/http" 26 "os" 27 "strconv" 28 "strings" 29 "time" 30 31 "go.opentelemetry.io/contrib/instrumentation/runtime" 32 "go.opentelemetry.io/otel" 33 "go.opentelemetry.io/otel/sdk/metric" 34 "go.opentelemetry.io/otel/sdk/trace" 35 36 "github.com/go-logr/zapr" 37 "go.uber.org/automaxprocs/maxprocs" // automatically set GOMAXPROCS based on cgroups 38 "go.uber.org/zap" 39 "golang.org/x/sync/errgroup" 40 41 corev1 "k8s.io/api/core/v1" 42 apierrors "k8s.io/apimachinery/pkg/api/errors" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/labels" 45 "k8s.io/apimachinery/pkg/util/sets" 46 "k8s.io/apimachinery/pkg/util/wait" 47 "k8s.io/client-go/rest" 48 k8stoolmetrics "k8s.io/client-go/tools/metrics" 49 "k8s.io/client-go/util/workqueue" 50 "k8s.io/klog/v2" 51 52 kubeclient "knative.dev/pkg/client/injection/kube/client" 53 "knative.dev/pkg/configmap" 54 cminformer "knative.dev/pkg/configmap/informer" 55 "knative.dev/pkg/controller" 56 "knative.dev/pkg/injection" 57 "knative.dev/pkg/leaderelection" 58 "knative.dev/pkg/logging" 59 "knative.dev/pkg/logging/logkey" 60 "knative.dev/pkg/observability" 61 o11yconfigmap "knative.dev/pkg/observability/configmap" 62 "knative.dev/pkg/observability/metrics" 63 k8smetrics "knative.dev/pkg/observability/metrics/k8s" 64 "knative.dev/pkg/observability/resource" 65 k8sruntime "knative.dev/pkg/observability/runtime/k8s" 66 "knative.dev/pkg/observability/tracing" 67 "knative.dev/pkg/reconciler" 68 "knative.dev/pkg/signals" 69 "knative.dev/pkg/system" 70 "knative.dev/pkg/version" 71 "knative.dev/pkg/webhook" 72 ) 73 74 func init() { 75 maxprocs.Set() 76 } 77 78 // GetLoggingConfig gets the logging config from the (in order): 79 // 1. provided context, 80 // 2. reading from the API server, 81 // 3. defaults (if not found). 82 // The context is expected to be initialized with injection. 83 func GetLoggingConfig(ctx context.Context) (*logging.Config, error) { 84 if cfg := logging.GetConfig(ctx); cfg != nil { 85 return cfg, nil 86 } 87 88 var loggingConfigMap *corev1.ConfigMap 89 // These timeout and retry interval are set by heuristics. 90 // e.g. istio sidecar needs a few seconds to configure the pod network. 91 var lastErr error 92 if err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 5*time.Second, true, func(ctx context.Context) (bool, error) { 93 loggingConfigMap, lastErr = kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()).Get(ctx, logging.ConfigMapName(), metav1.GetOptions{}) 94 return lastErr == nil || apierrors.IsNotFound(lastErr), nil 95 }); err != nil { 96 return nil, fmt.Errorf("timed out waiting for the condition: %w", lastErr) 97 } 98 if loggingConfigMap == nil { 99 return logging.NewConfigFromMap(nil) 100 } 101 return logging.NewConfigFromConfigMap(loggingConfigMap) 102 } 103 104 // GetLeaderElectionConfig gets the leader election config from the (in order): 105 // 1. provided context, 106 // 2. reading from the API server, 107 // 3. defaults (if not found). 108 func GetLeaderElectionConfig(ctx context.Context) (*leaderelection.Config, error) { 109 if cfg := leaderelection.GetConfig(ctx); cfg != nil { 110 return cfg, nil 111 } 112 113 leaderElectionConfigMap, err := kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()).Get(ctx, leaderelection.ConfigMapName(), metav1.GetOptions{}) 114 if apierrors.IsNotFound(err) { 115 return leaderelection.NewConfigFromConfigMap(nil) 116 } else if err != nil { 117 return nil, err 118 } 119 return leaderelection.NewConfigFromConfigMap(leaderElectionConfigMap) 120 } 121 122 // GetObservabilityConfig gets the observability config from the (in order): 123 // 1. provided context, 124 // 2. reading from the API server, 125 // 3. defaults (if not found). 126 func GetObservabilityConfig(ctx context.Context) (*observability.Config, error) { 127 if cfg := observability.GetConfig(ctx); cfg != nil { 128 return cfg, nil 129 } 130 131 client := kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()) 132 cm, err := client.Get(ctx, o11yconfigmap.Name(), metav1.GetOptions{}) 133 134 if apierrors.IsNotFound(err) { 135 return observability.DefaultConfig(), nil 136 } else if err != nil { 137 return nil, err 138 } 139 140 return o11yconfigmap.Parse(cm) 141 } 142 143 // EnableInjectionOrDie enables Knative Injection and starts the informers. 144 // Both Context and Config are optional. 145 // Deprecated: use injection.EnableInjectionOrDie 146 func EnableInjectionOrDie(ctx context.Context, cfg *rest.Config) context.Context { 147 ctx, startInformers := injection.EnableInjectionOrDie(ctx, cfg) 148 go startInformers() 149 return ctx 150 } 151 152 // Main runs the generic main flow with a new context. 153 // If any of the constructed controllers are AdmissionControllers or Conversion 154 // webhooks, then a webhook is started to serve them. 155 func Main(component string, ctors ...injection.ControllerConstructor) { 156 // Set up signals so we handle the first shutdown signal gracefully. 157 MainWithContext(signals.NewContext(), component, ctors...) 158 } 159 160 // Legacy aliases for back-compat. 161 var ( 162 WebhookMainWithContext = MainWithContext 163 WebhookMainWithConfig = MainWithConfig 164 ) 165 166 // MainNamed runs the generic main flow for controllers and webhooks. 167 // 168 // In addition to the MainWithConfig flow, it defines a `disabled-controllers` flag that allows disabling controllers 169 // by name. 170 func MainNamed(ctx context.Context, component string, ctors ...injection.NamedControllerConstructor) { 171 disabledControllers := flag.String("disable-controllers", "", "Comma-separated list of disabled controllers.") 172 173 // HACK: This parses flags, so the above should be set once this runs. 174 cfg := injection.ParseAndGetRESTConfigOrDie() 175 176 enabledCtors := enabledControllers(strings.Split(*disabledControllers, ","), ctors) 177 MainWithConfig(ctx, component, cfg, toControllerConstructors(enabledCtors)...) 178 } 179 180 func enabledControllers(disabledControllers []string, ctors []injection.NamedControllerConstructor) []injection.NamedControllerConstructor { 181 disabledControllersSet := sets.NewString(disabledControllers...) 182 activeCtors := make([]injection.NamedControllerConstructor, 0, len(ctors)) 183 for _, ctor := range ctors { 184 if disabledControllersSet.Has(ctor.Name) { 185 log.Printf("Disabling controller %s", ctor.Name) 186 continue 187 } 188 activeCtors = append(activeCtors, ctor) 189 } 190 return activeCtors 191 } 192 193 func toControllerConstructors(namedCtors []injection.NamedControllerConstructor) []injection.ControllerConstructor { 194 ctors := make([]injection.ControllerConstructor, 0, len(namedCtors)) 195 for _, ctor := range namedCtors { 196 ctors = append(ctors, ctor.ControllerConstructor) 197 } 198 return ctors 199 } 200 201 // MainWithContext runs the generic main flow for controllers and 202 // webhooks. Use MainWithContext if you do not need to serve webhooks. 203 func MainWithContext(ctx context.Context, component string, ctors ...injection.ControllerConstructor) { 204 // Allow configuration of threads per controller 205 if val, ok := os.LookupEnv("K_THREADS_PER_CONTROLLER"); ok { 206 threadsPerController, err := strconv.Atoi(val) 207 if err != nil { 208 log.Fatalf("failed to parse value %q of K_THREADS_PER_CONTROLLER: %v\n", val, err) 209 } 210 controller.DefaultThreadsPerController = threadsPerController 211 } 212 213 // TODO(mattmoor): Remove this once HA is stable. 214 disableHighAvailability := flag.Bool("disable-ha", false, 215 "Whether to disable high-availability functionality for this component. This flag will be deprecated "+ 216 "and removed when we have promoted this feature to stable, so do not pass it without filing an "+ 217 "issue upstream!") 218 219 // HACK: This parses flags, so the above should be set once this runs. 220 cfg := injection.ParseAndGetRESTConfigOrDie() 221 222 if *disableHighAvailability { 223 ctx = WithHADisabled(ctx) 224 } 225 226 MainWithConfig(ctx, component, cfg, ctors...) 227 } 228 229 type haDisabledKey struct{} 230 231 // WithHADisabled signals to MainWithConfig that it should not set up an appropriate leader elector for this component. 232 func WithHADisabled(ctx context.Context) context.Context { 233 return context.WithValue(ctx, haDisabledKey{}, struct{}{}) 234 } 235 236 // IsHADisabled checks the context for the desired to disabled leader elector. 237 func IsHADisabled(ctx context.Context) bool { 238 return ctx.Value(haDisabledKey{}) != nil 239 } 240 241 // MainWithConfig runs the generic main flow for controllers and webhooks 242 // with the given config. 243 func MainWithConfig(ctx context.Context, component string, cfg *rest.Config, ctors ...injection.ControllerConstructor) { 244 log.Printf("Registering %d clients", len(injection.Default.GetClients())) 245 log.Printf("Registering %d informer factories", len(injection.Default.GetInformerFactories())) 246 log.Printf("Registering %d informers", len(injection.Default.GetInformers())) 247 log.Printf("Registering %d controllers", len(ctors)) 248 249 // Respect user provided settings, but if omitted customize the default behavior. 250 if cfg.QPS == 0 { 251 // Adjust our client's rate limits based on the number of controllers we are running. 252 cfg.QPS = float32(len(ctors)) * rest.DefaultQPS 253 } 254 if cfg.Burst == 0 { 255 cfg.Burst = len(ctors) * rest.DefaultBurst 256 } 257 258 ctx, startInformers := injection.EnableInjectionOrDie(ctx, cfg) 259 260 logger, atomicLevel := SetupLoggerOrDie(ctx, component) 261 defer logger.Sync() 262 ctx = logging.WithLogger(ctx, logger) 263 264 klog.SetLogger(zapr.NewLogger(logger.Desugar())) 265 266 // Override client-go's warning handler to give us nicely printed warnings. 267 rest.SetDefaultWarningHandler(&logging.WarningHandler{Logger: logger}) 268 269 pprof := k8sruntime.NewProfilingServer(logger.Named("pprof")) 270 271 CheckK8sClientMinimumVersionOrDie(ctx, logger) 272 cmw := SetupConfigMapWatchOrDie(ctx, logger) 273 274 // Set up leader election config 275 leaderElectionConfig, err := GetLeaderElectionConfig(ctx) 276 if err != nil { 277 logger.Fatal("Error loading leader election configuration: ", err) 278 } 279 280 if !IsHADisabled(ctx) { 281 // Signal that we are executing in a context with leader election. 282 ctx = leaderelection.WithDynamicLeaderElectorBuilder(ctx, kubeclient.Get(ctx), 283 leaderElectionConfig.GetComponentConfig(component)) 284 } 285 286 mp, tp := SetupObservabilityOrDie(ctx, component, logger, pprof) 287 defer func() { 288 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 289 defer cancel() 290 291 if err := mp.Shutdown(ctx); err != nil { 292 logger.Errorw("Error flushing metrics", zap.Error(err)) 293 } 294 if err := tp.Shutdown(ctx); err != nil { 295 logger.Errorw("Error flushing traces", zap.Error(err)) 296 } 297 }() 298 299 controllers, webhooks := ControllersAndWebhooksFromCtors(ctx, cmw, ctors...) 300 WatchLoggingConfigOrDie(ctx, cmw, logger, atomicLevel, component) 301 WatchObservabilityConfigOrDie(ctx, cmw, pprof, logger, component) 302 303 eg, egCtx := errgroup.WithContext(ctx) 304 eg.Go(pprof.ListenAndServe) 305 306 // Many of the webhooks rely on configuration, e.g. configurable defaults, feature flags. 307 // So make sure that we have synchronized our configuration state before launching the 308 // webhooks, so that things are properly initialized. 309 logger.Info("Starting configmap watcher...") 310 if err := cmw.Start(ctx.Done()); err != nil { 311 logger.Fatalw("Failed to start configmap watcher", zap.Error(err)) 312 } 313 314 // If we have one or more admission controllers, then start the webhook 315 // and pass them in. 316 var wh *webhook.Webhook 317 if len(webhooks) > 0 { 318 wh, err = webhook.New(ctx, webhooks) 319 if err != nil { 320 logger.Fatalw("Failed to create webhook", zap.Error(err)) 321 } 322 eg.Go(func() error { 323 return wh.Run(ctx.Done()) 324 }) 325 } 326 327 // Start the injection clients and informers. 328 startInformers() 329 330 // Wait for webhook informers to sync. 331 if wh != nil { 332 wh.InformersHaveSynced() 333 } 334 logger.Info("Starting controllers...") 335 eg.Go(func() error { 336 return controller.StartAll(ctx, controllers...) 337 }) 338 339 // Setup default health checks to catch issues with cache sync etc. 340 if !healthProbesDisabled(ctx) { 341 eg.Go(func() error { 342 return injection.ServeHealthProbes(ctx, injection.HealthCheckDefaultPort) 343 }) 344 } 345 346 // This will block until either a signal arrives or one of the grouped functions 347 // returns an error. 348 <-egCtx.Done() 349 350 pprof.Shutdown(context.Background()) 351 352 // Don't forward ErrServerClosed as that indicates we're already shutting down. 353 if err := eg.Wait(); err != nil && !errors.Is(err, http.ErrServerClosed) { 354 logger.Errorw("Error while running server", zap.Error(err)) 355 } 356 } 357 358 type healthProbesDisabledKey struct{} 359 360 // WithHealthProbesDisabled signals to MainWithContext that it should disable default probes (readiness and liveness). 361 func WithHealthProbesDisabled(ctx context.Context) context.Context { 362 return context.WithValue(ctx, healthProbesDisabledKey{}, struct{}{}) 363 } 364 365 func healthProbesDisabled(ctx context.Context) bool { 366 return ctx.Value(healthProbesDisabledKey{}) != nil 367 } 368 369 // SetupLoggerOrDie sets up the logger using the config from the given context 370 // and returns a logger and atomic level, or dies by calling log.Fatalf. 371 func SetupLoggerOrDie(ctx context.Context, component string) (*zap.SugaredLogger, zap.AtomicLevel) { 372 loggingConfig, err := GetLoggingConfig(ctx) 373 if err != nil { 374 log.Fatal("Error reading/parsing logging configuration: ", err) 375 } 376 l, level := logging.NewLoggerFromConfig(loggingConfig, component) 377 378 if pn := system.PodName(); pn != "" { 379 l = l.With(zap.String(logkey.Pod, pn)) 380 } 381 382 return l, level 383 } 384 385 // SetupObservabilityOrDie sets up the observability using the config from the given context 386 // or dies by calling log.Fatalf. 387 func SetupObservabilityOrDie( 388 ctx context.Context, 389 component string, 390 logger *zap.SugaredLogger, 391 pprof *k8sruntime.ProfilingServer, 392 ) (*metrics.MeterProvider, *tracing.TracerProvider) { 393 cfg, err := GetObservabilityConfig(ctx) 394 if err != nil { 395 logger.Fatal("Error loading observability configuration: ", err) 396 } 397 398 pprof.UpdateFromConfig(cfg.Runtime) 399 400 resource := resource.Default(component) 401 402 meterProvider, err := metrics.NewMeterProvider( 403 ctx, 404 cfg.Metrics, 405 metric.WithView(OTelViews(ctx)...), 406 metric.WithResource(resource), 407 ) 408 if err != nil { 409 logger.Fatalw("Failed to setup meter provider", zap.Error(err)) 410 } 411 412 otel.SetMeterProvider(meterProvider) 413 414 workQueueMetrics, err := k8smetrics.NewWorkqueueMetricsProvider( 415 k8smetrics.WithMeterProvider(meterProvider), 416 ) 417 if err != nil { 418 logger.Fatalw("Failed to setup k8s workqueue metrics", zap.Error(err)) 419 } 420 421 workqueue.SetProvider(workQueueMetrics) 422 controller.SetMetricsProvider(workQueueMetrics) 423 424 clientMetrics, err := k8smetrics.NewClientMetricProvider( 425 k8smetrics.WithMeterProvider(meterProvider), 426 ) 427 if err != nil { 428 logger.Fatalw("Failed to setup k8s client go metrics", zap.Error(err)) 429 } 430 431 k8stoolmetrics.Register(k8stoolmetrics.RegisterOpts{ 432 RequestLatency: clientMetrics.RequestLatencyMetric(), 433 RequestResult: clientMetrics.RequestResultMetric(), 434 }) 435 436 err = runtime.Start( 437 runtime.WithMinimumReadMemStatsInterval(cfg.Runtime.ExportInterval), 438 ) 439 if err != nil { 440 logger.Fatalw("Failed to start runtime metrics", zap.Error(err)) 441 } 442 443 tracerProvider, err := tracing.NewTracerProvider( 444 ctx, 445 cfg.Tracing, 446 trace.WithResource(resource), 447 ) 448 if err != nil { 449 logger.Fatalw("Failed to setup trace provider", zap.Error(err)) 450 } 451 452 otel.SetTextMapPropagator(tracing.DefaultTextMapPropagator()) 453 otel.SetTracerProvider(tracerProvider) 454 455 return meterProvider, tracerProvider 456 } 457 458 // CheckK8sClientMinimumVersionOrDie checks that the hosting Kubernetes cluster 459 // is at least the minimum allowable version or dies by calling log.Fatalw. 460 func CheckK8sClientMinimumVersionOrDie(ctx context.Context, logger *zap.SugaredLogger) { 461 kc := kubeclient.Get(ctx) 462 if err := version.CheckMinimumVersion(kc.Discovery()); err != nil { 463 logger.Fatalw("Version check failed", zap.Error(err)) 464 } 465 } 466 467 // SetupConfigMapWatchOrDie establishes a watch of the configmaps in the system 468 // namespace that are labeled to be watched or dies by calling log.Fatalw. 469 func SetupConfigMapWatchOrDie(ctx context.Context, logger *zap.SugaredLogger) *cminformer.InformedWatcher { 470 kc := kubeclient.Get(ctx) 471 // Create ConfigMaps watcher with optional label-based filter. 472 var cmLabelReqs []labels.Requirement 473 if cmLabel := system.ResourceLabel(); cmLabel != "" { 474 req, err := cminformer.FilterConfigByLabelExists(cmLabel) 475 if err != nil { 476 logger.Fatalw("Failed to generate requirement for label "+cmLabel, zap.Error(err)) 477 } 478 logger.Info("Setting up ConfigMap watcher with label selector: ", req) 479 cmLabelReqs = append(cmLabelReqs, *req) 480 } 481 // TODO(mattmoor): This should itself take a context and be injection-based. 482 return cminformer.NewInformedWatcher(kc, system.Namespace(), cmLabelReqs...) 483 } 484 485 // WatchLoggingConfigOrDie establishes a watch of the logging config or dies by 486 // calling log.Fatalw. Note, if the config does not exist, it will be defaulted 487 // and this method will not die. 488 func WatchLoggingConfigOrDie(ctx context.Context, cmw *cminformer.InformedWatcher, logger *zap.SugaredLogger, atomicLevel zap.AtomicLevel, component string) { 489 if _, err := kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()).Get(ctx, logging.ConfigMapName(), 490 metav1.GetOptions{}); err == nil { 491 cmw.Watch(logging.ConfigMapName(), logging.UpdateLevelFromConfigMap(logger, atomicLevel, component)) 492 } else if !apierrors.IsNotFound(err) { 493 logger.Fatalw("Error reading ConfigMap "+logging.ConfigMapName(), zap.Error(err)) 494 } 495 } 496 497 // WatchObservabilityConfigOrDie establishes a watch of the observability config 498 // or dies by calling log.Fatalw. Note, if the config does not exist, it will be 499 // defaulted and this method will not die. 500 func WatchObservabilityConfigOrDie( 501 ctx context.Context, 502 cmw *cminformer.InformedWatcher, 503 pprof *k8sruntime.ProfilingServer, 504 logger *zap.SugaredLogger, 505 component string, 506 ) { 507 cmName := o11yconfigmap.Name() 508 client := kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()) 509 510 observers := []configmap.Observer{ 511 pprof.UpdateFromConfigMap, 512 } 513 514 if _, err := client.Get(ctx, cmName, metav1.GetOptions{}); err == nil { 515 cmw.Watch(cmName, observers...) 516 } else if !apierrors.IsNotFound(err) { 517 logger.Fatalw("Error reading ConfigMap "+cmName, zap.Error(err)) 518 } 519 } 520 521 // ControllersAndWebhooksFromCtors returns a list of the controllers and a list 522 // of the webhooks created from the given constructors. 523 func ControllersAndWebhooksFromCtors(ctx context.Context, 524 cmw *cminformer.InformedWatcher, 525 ctors ...injection.ControllerConstructor, 526 ) ([]*controller.Impl, []any) { 527 // Check whether the context has been infused with a leader elector builder. 528 // If it has, then every reconciler we plan to start MUST implement LeaderAware. 529 leEnabled := leaderelection.HasLeaderElection(ctx) 530 531 controllers := make([]*controller.Impl, 0, len(ctors)) 532 webhooks := make([]any, 0) 533 for _, cf := range ctors { 534 ctrl := cf(ctx, cmw) 535 controllers = append(controllers, ctrl) 536 537 // Build a list of any reconcilers that implement webhook.AdmissionController 538 switch c := ctrl.Reconciler.(type) { 539 case webhook.AdmissionController, webhook.ConversionController: 540 webhooks = append(webhooks, c) 541 } 542 543 if leEnabled { 544 if _, ok := ctrl.Reconciler.(reconciler.LeaderAware); !ok { 545 log.Fatalf("%T is not leader-aware, all reconcilers must be leader-aware to enable fine-grained leader election.", ctrl.Reconciler) 546 } 547 } 548 } 549 550 return controllers, webhooks 551 }