github.com/cilium/cilium@v1.16.2/Documentation/contributing/development/hive.rst (about) 1 .. only:: not (epub or latex or html) 2 3 WARNING: You are looking at unreleased Cilium documentation. 4 Please use the official rendered version released here: 5 https://docs.cilium.io 6 7 .. _guide-to-the-hive: 8 9 Guide to the Hive 10 ================= 11 12 Introduction 13 ~~~~~~~~~~~~ 14 15 Cilium is using dependency injection (via ``pkg/hive``) to wire up the 16 initialization, starting and stopping of its components. 17 18 `Dependency injection <https://en.wikipedia.org/wiki/Dependency_injection>`_ (DI) is a 19 technique for separating the use of objects from their creation and 20 initialization. Essentially dependency injection is about automating the 21 manual management of dependencies. Object constructors only need to declare 22 their dependencies as function parameters and the rest is handled by the library. This 23 helps with building a loosely-coupled modular architecture as it removes the 24 need for centralization of initialization and configuration. It also reduces 25 the inclination to use global variables over explicit passing of objects, 26 which is often a source of bugs (due to unexpected initialization order) 27 and difficult to deal with in tests (as the state needs to be restored for 28 the next test). With dependency injection components are described as plain 29 values (``Cell`` in our flavor of DI) enabling visualization of inter-component 30 dependencies and opening the internal architecture up for inspection. 31 32 Dependency injection and the machinery described here are only a tool to 33 help us towards the real goal: a modular software architecture that can be 34 easily understood, extended, repurposed, tested and refactored by a large 35 group of developers with minimal overlap between modules. To achieve this we 36 also need to have modularity in mind when designing the architecture and APIs. 37 38 Hive and Cells 39 ~~~~~~~~~~~~~~ 40 41 Cilium applications are composed using runtime dependency injection from a set 42 of modular components called cells that compose together to form a hive (as in 43 bee hive). A hive can then be supplied with configuration and executed. To provide 44 a feel for what this is about, here is how a simple modular HTTP server application 45 would leverage hive: 46 47 .. code-block:: go 48 49 package server 50 51 // The server cell implements a generic HTTP server. Provides the 'Server' API 52 // for registering request handlers. 53 // 54 // Module() creates a named collection of cells. 55 var Cell = cell.Module( 56 "http-server", // Module identifier (for e.g. logging and tracing) 57 "HTTP Server", // Module title (for documentation) 58 59 // Provide the application the constructor for the server. 60 cell.Provide(New), 61 62 // Config registers a configuration when provided with the defaults 63 // and an implementation of Flags() for registering the configuration flags. 64 cell.Config(defaultServerConfig), 65 ) 66 67 // Server allows registering request handlers with the HTTP server 68 type Server interface { 69 ListenAddress() string 70 RegisterHandler(path string, fn http.HandlerFunc) 71 } 72 73 func New(lc cell.Lifecycle, cfg ServerConfig) Server { 74 // Initialize http.Server, register Start and Stop hooks to Lifecycle 75 // for starting and stopping the server and return an implementation of 76 // 'Server' for other cells for registering handlers. 77 // ... 78 } 79 80 type ServerConfig struct { 81 ServerPort uint16 82 } 83 84 var defaultServerConfig = ServerConfig{ 85 ServerPort: 8080, 86 } 87 88 func (def ServerConfig) Flags(flags *pflag.FlagSet) { 89 // Register the "server-port" flag. Hive by convention maps the flag to the ServerPort 90 // field. 91 flags.Uint16("server-port", def.ServerPort, "Sets the HTTP server listen port") 92 } 93 94 With the above generic HTTP server in the ``server`` package, we can now implement a simple handler 95 for /hello in the ``hello`` package: 96 97 .. code-block:: go 98 99 package hello 100 101 // The hello cell implements and registers a hello handler to the HTTP server. 102 // 103 // This cell isn't a Module, but rather just a plain Invoke. An Invoke 104 // is a cell that, unlike Provide, is always executed. Invoke functions 105 // can depend on values that constructors registered with Provide() can 106 // return. These constructors are then called and their results remembered. 107 var Cell = cell.Invoke(registerHelloHandler) 108 109 func helloHandler(w http.ResponseWriter, req *http.Request) { 110 w.Write([]byte("hello")) 111 } 112 113 func registerHelloHandler(srv server.Server) { 114 srv.RegisterHandler("/hello", helloHandler) 115 } 116 117 And then put the two together into a simple application: 118 119 .. code-block:: go 120 121 package main 122 123 var ( 124 // exampleHive is an application with an HTTP server and a handler 125 // at /hello. 126 exampleHive = hive.New( 127 server.Cell, 128 hello.Cell, 129 ) 130 131 // cmd is the root command for this application. Runs 132 // exampleHive when executed. 133 cmd *cobra.Command = &cobra.Command{ 134 Use: "example", 135 Run: func(cmd *cobra.Command, args []string) { 136 // Run() will execute all invoke functions, followed by start hooks 137 // and will then wait for interrupt signal before executing stop hooks 138 // and returning. 139 exampleHive.Run() 140 }, 141 } 142 ) 143 144 func main() { 145 // Register all command-line flags from each config cell to the 146 // flag-set of our command. 147 exampleHive.RegisterFlags(cmd.Flags()) 148 149 // Add the "hive" sub-command for inspecting the application. 150 cmd.AddCommand(exampleHive.Command())) 151 152 // Execute the root command. 153 cmd.Execute() 154 } 155 156 157 If you prefer to learn by example you can find a more complete and runnable example 158 application from ``pkg/hive/example``. Try running it with ``go run .`` and also try 159 ``go run . hive``. And if you're interested in how all this is implemented internally, 160 see ``pkg/hive/example/mini``, a minimal example of how to do dependency injection with reflection. 161 162 The Hive API 163 ~~~~~~~~~~~~ 164 165 With the example hopefully having now whetted the appetite, we'll take a proper look at 166 the hive API. 167 168 `pkg/hive <https://pkg.go.dev/github.com/cilium/cilium/pkg/hive>`_ provides the Hive type and 169 `hive.New <https://pkg.go.dev/github.com/cilium/cilium/pkg/hive#New>`_ constructor. 170 The ``hive.Hive`` type can be thought of as an application container, composed from cells: 171 172 .. code-block:: go 173 174 var myHive = hive.New(foo.Cell, bar.Cell) 175 176 // Call Run() to run the hive. 177 myHive.Run() // Start(), wait for signal (ctrl-c) and then Stop() 178 179 // Hive can also be started and stopped directly. Useful in tests. 180 if err := myHive.Start(ctx); err != nil { /* ... */ } 181 if err := myHive.Stop(ctx); err != nil { /* ... */ } 182 183 // Hive's configuration can be registered with a Cobra command: 184 hive.RegisterFlags(cmd.Flags()) 185 186 // Hive also provides a sub-command for inspecting it: 187 cmd.AddCommand(hive.Command()) 188 189 `pkg/hive/cell <https://pkg.go.dev/github.com/cilium/cilium/pkg/hive/cell>`_ defines the Cell interface that 190 ``hive.New()`` consumes and the following functions for creating cells: 191 192 - :ref:`api_module`: A named set of cells. 193 - :ref:`api_provide`: Provides constructor(s) to the hive. Lazy and only invoked if referenced by an Invoke function (directly or indirectly via other constructor). 194 - :ref:`ProvidePrivate <api_module>`: Provides private constructor(s) to a module and its sub-modules. 195 - :ref:`api_decorate`: Wraps a set of cells with a decorator function to provide these cells with augmented objects. 196 - :ref:`api_config`: Provides a configuration struct to the hive. 197 - :ref:`api_invoke`: Registers an invoke function to instantiate and initialize objects. 198 - :ref:`api_metric`: Provides metrics to the hive. 199 200 Hive also by default provides the following globally available objects: 201 202 - :ref:`api_lifecycle`: Methods for registering Start and Stop functions that are executed when Hive is started and stopped. 203 The hooks are appended to it in dependency order (since the constructors are invoked in dependency order). 204 - :ref:`api_shutdowner`: Allows gracefully shutting down the hive from anywhere in case of a fatal error post-start. 205 - ``logrus.FieldLogger``: Interface to the logger. Module() decorates it with ``subsys=<module id>``. 206 207 .. _api_provide: 208 209 Provide 210 ^^^^^^^ 211 212 We'll now take a look at each of the different kinds of cells, starting with Provide(), 213 which registers one or more constructors with the hive: 214 215 .. code-block:: go 216 217 // func Provide(ctors any...) Cell 218 219 type A interface {} 220 func NewA() A { return A{} } 221 222 type B interface {} 223 func NewB(A) B { return B{} } 224 225 // simpleCell provides A and B 226 var simpleCell cell.Cell = cell.Provide(NewA, NewB) 227 228 If the constructors take many parameters, we'll want to group them into a struct with ``cell.In``, 229 and conversely if there are many return values, into a struct with ``cell.Out``. This tells 230 hive to unpack them: 231 232 .. code-block:: go 233 234 type params struct { 235 cell.In 236 237 A A 238 B B 239 Lifecycle cell.Lifecycle 240 } 241 242 type out struct { 243 cell.Out 244 245 C C 246 D D 247 E E 248 } 249 func NewCDE(params params) out { ... } 250 251 var Cell = cell.Provide(NewCDE) 252 253 Sometimes we want to depend on a group of values sharing the same type, e.g. to collect API handlers or metrics. This can be done with 254 `value groups <https://pkg.go.dev/go.uber.org/dig#hdr-Value_Groups>`_ by combining ``cell.In`` 255 and ``cell.Out`` with the ``group`` struct tag: 256 257 .. code-block:: go 258 259 type HandlerOut struct { 260 cell.Out 261 262 Handler Handler `group:"handlers"` 263 } 264 func NewHelloHandler() HandlerOut { ... } 265 func NewEventHandler(src events.Source) HandlerOut { ... } 266 267 type ServerParams struct { 268 cell.In 269 270 Handlers []Handler `group:"handlers"` 271 } 272 273 func NewServer(params ServerParams) Server { 274 // params.Handlers will have the "Handlers" from NewHelloHandler and 275 // NewEventHandler. 276 } 277 278 var Hive = hive.New( 279 cell.Provide(NewHelloHandler, NewEventHandler, NewServer) 280 ) 281 282 For a working example of group values this, see ``pkg/hive/example``. 283 284 Use ``Provide()`` when you want to expose an object or an interface to the application. If there is nothing meaningful 285 to expose, consider instead using ``Invoke()`` to register lifecycle hooks for an unexported object. 286 287 .. _api_invoke: 288 289 Invoke 290 ^^^^^^ 291 292 Invoke is used to invoke a function to initialize some part of the application. The provided constructors 293 won't be called unless an invoke function references them, either directly or indirectly via another 294 constructor: 295 296 .. code-block:: go 297 298 // func Invoke(funcs ...any) Cell 299 300 cell.Invoke( 301 // Construct both B and C and then introduce them to each other. 302 func(b B, c C) { 303 b.SetHandler(c) 304 c.SetOwner(b) 305 }, 306 307 // Construct D for its side-effects only (e.g. start and stop hooks). 308 // Avoid this if you can and use Invoke() to register hooks instead of Provide() if 309 // there's no API to provide. 310 func(D){}, 311 ) 312 313 .. _api_module: 314 315 Module 316 ^^^^^^ 317 318 Cells can be grouped into modules (a named set of cells): 319 320 .. code-block:: go 321 322 // func Module(id, title string, cells ...Cell) Cell 323 324 var Cell = cell.Module( 325 "example", // short identifier (for use in e.g. logging and tracing) 326 "An example module", // one-line description (for documentation) 327 328 cell.Provide(New), 329 330 innerModule, // modules can contain other modules 331 ) 332 333 var innerModule cell.Cell = cell.Module( 334 "example-inner", 335 "An inner module", 336 337 cell.Provide(newInner), 338 ) 339 340 341 Module() also provides the wrapped cells with a personalized ``logrus.FieldLogger`` 342 with the ``subsys`` field set to module identifier ("example" above). 343 344 The scope created by Module() is useful when combined with ProvidePrivate(): 345 346 .. code-block:: go 347 348 var Cell = cell.Module( 349 "example", 350 "An example module", 351 352 cell.ProvidePrivate(NewA), // A only accessible from this module (or sub-modules) 353 cell.Provide(NewB), // B is accessible from anywhere 354 ) 355 356 .. _api_decorate: 357 358 Decorate 359 ^^^^^^^^ 360 361 Sometimes one may want to use a modified object inside a module, for example how above Module() 362 provided the cells with a personalized logger. This can be done with a decorator: 363 364 .. code-block:: go 365 366 // func Decorate(dtor any, cells ...Cell) Cell 367 368 var Cell = cell.Decorate( 369 myLogger, // The decoration function 370 371 // These cells will see the objects returned by the 'myLogger' decorator 372 // rather than the objects on the outside. 373 foo.Cell, 374 bar.Cell, 375 ) 376 377 // myLogger is a decorator that can depend on one or more objects in the application 378 // and return one or more objects. The input parameters don't necessarily need to match 379 // the output types. 380 func myLogger(log logrus.FieldLogger) logrus.FieldLogger { 381 return log.WithField("lasers", "stun") 382 } 383 384 385 .. _api_config: 386 387 Config 388 ^^^^^^ 389 390 Cilium applications use the `cobra <https://github.com/spf13/cobra>`_ and 391 `pflag <https://github.com/spf13/pflag>`_ libraries for implementing the command-line 392 interface. With Cobra, one defines a ``Command``, with optional sub-commands. Each command 393 has an associated FlagSet which must be populated before a command is executed in order to 394 parse or to produce usage documentation. Hive bridges to Cobra with ``cell.Config``, which 395 takes a value that implements ``cell.Flagger`` for adding flags to a command's FlagSet and 396 returns a cell that "provides" the parsed configuration to the application: 397 398 .. code-block:: go 399 400 // type Flagger interface { 401 // Flags(flags *pflag.FlagSet) 402 // } 403 // func Config[Cfg Flagger](defaultConfig Cfg) cell.Cell 404 405 type MyConfig struct { 406 MyOption string 407 408 SliceOption []string 409 MapOption map[string]string 410 } 411 412 func (def MyConfig) Flags(flags *pflag.FlagSet) { 413 // Register the "my-option" flag. This matched against the MyOption field 414 // by removing any dashes and doing case insensitive comparison. 415 flags.String("my-option", def.MyOption, "My config option") 416 417 // Flags are supported for representing complex types such as slices and maps. 418 // * Slices are obtained splitting the input string on commas. 419 // * Maps support different formats based on how they are provided: 420 // - CLI: key=value format, separated by commas; the flag can be 421 // repeated multiple times. 422 // - Environment variable or configuration file: either JSON encoded 423 // or comma-separated key=value format. 424 flags.StringSlice("slice-option", def.SliceOption, "My slice config option") 425 flags.StringToString("map-option", def.MapOption, "My map config option") 426 } 427 428 var defaultMyConfig = MyConfig{ 429 MyOption: "the default value", 430 } 431 432 func New(cfg MyConfig) MyThing 433 434 var Cell = cell.Module( 435 "module-with-config", 436 "A module with a config", 437 438 cell.Config(defaultMyConfig), 439 cell.Provide(New), 440 ) 441 442 In tests the configuration can be populated in various ways: 443 444 .. code-block:: go 445 446 func TestCell(t *testing.T) { 447 h := hive.New(Cell) 448 449 // Options can be set via Viper 450 h.Viper().Set("my-option", "test-value") 451 452 // Or via pflags 453 flags := pflag.NewFlagSet("", pflag.ContinueOnError) 454 h.RegisterFlags(flags) 455 flags.Set("my-option", "test-value") 456 flags.Parse("--my-option=test-value") 457 458 // Or the preferred way with a config override: 459 h = hive.New( 460 Cell, 461 ) 462 AddConfigOverride( 463 h, 464 func(cfg *MyConfig) { 465 cfg.MyOption = "test-override" 466 }) 467 468 // To validate that the Cell can be instantiated and the configuration 469 // struct is well-formed without starting you can call Populate(): 470 if err := h.Populate(); err != nil { 471 t.Fatalf("Failed to populate: %s", err) 472 } 473 } 474 475 .. _api_metric: 476 477 Metric 478 ^^^^^^ 479 480 The metric cell allows you to define a collection of metrics near a feature you 481 would like to instrument. Like the :ref:`api_provide` cell, you define a new 482 type and a constructor. In the case of a metric cell the type should be a 483 struct with only public fields. The types of these fields should implement 484 both `metric.WithMetadata <https://pkg.go.dev/github.com/cilium/cilium/pkg/metrics/metric#WithMetadata>`_ 485 and `prometheus.Collector <https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Collector>`_. 486 The easiest way to get such metrics is to use the types defined in `pkg/metrics/metric <https://pkg.go.dev/github.com/cilium/cilium/pkg/metrics/metric>`_. 487 488 The metric collection struct type returned by the given constructor is made 489 available in the hive just like a normal provide. In addition all of the metrics 490 are made available via the ``hive-metrics`` `value group <https://pkg.go.dev/go.uber.org/dig#hdr-Value_Groups>`_. 491 This value group is consumed by the metrics package so any metrics defined 492 via a metric cell are automatically registered. 493 494 .. code-block:: go 495 496 var Cell = cell.Module("my-feature", "My Feature", 497 cell.Metric(NewFeatureMetrics), 498 cell.Provide(NewMyFeature), 499 ) 500 501 type FeatureMetrics struct { 502 Calls metric.Vec[metric.Counter] 503 Latency metric.Histogram 504 } 505 506 func NewFeatureMetrics() FeatureMetrics { 507 return FeatureMetrics{ 508 Calls: metric.NewCounterVec(metric.CounterOpts{ 509 ConfigName: metrics.Namespace + "_my_feature_calls_total", 510 Subsystem: "my_feature", 511 Namespace: metrics.Namespace, 512 Name: "calls_total", 513 }, []string{"caller"}), 514 Latency: metric.NewHistogram(metric.HistogramOpts{ 515 ConfigName: metrics.Namespace + "_my_feature_latency_seconds", 516 Namespace: metrics.Namespace, 517 Subsystem: "my_feature", 518 Name: "latency_seconds", 519 }), 520 } 521 } 522 523 type MyFeature struct { 524 metrics FeatureMetrics 525 } 526 527 func NewMyFeature(metrics FeatureMetrics) *MyFeature { 528 return &MyFeature{ 529 metrics: metrics, 530 } 531 } 532 533 func (mf *MyFeature) SomeFunction(caller string) { 534 mf.metrics.Calls.With(prometheus.Labels{"caller": caller}).Inc() 535 536 span := spanstat.Start() 537 // Normally we would do some actual work here 538 time.Sleep(time.Second) 539 span.End(true) 540 541 mf.metrics.Latency.Observe(span.Seconds()) 542 } 543 544 .. _api_lifecycle: 545 546 Lifecycle 547 ^^^^^^^^^ 548 549 In addition to cells an important building block in hive is the lifecycle. A 550 lifecycle is a list of start and stop hook pairs that are executed in order 551 (reverse when stopping) when running the hive. 552 553 .. code-block:: go 554 555 package hive 556 557 type Lifecycle { 558 Append(HookInterface) 559 } 560 type HookContext context.Context 561 562 type HookInterface interface { 563 Start(HookContext) error 564 Stop(HookContext) error 565 } 566 567 type Hook struct { 568 OnStart func(HookContext) error 569 OnStop func(HookContext) error 570 } 571 572 func (h Hook) Start(ctx HookContext) error { ... } 573 func (h Hook) Stop(ctx HookContext) error { ... } 574 575 The lifecycle hooks can be implemented either by implementing the HookInterface methods, 576 or using the Hook struct. Lifecycle is accessible from any cell: 577 578 .. code-block:: go 579 580 var ExampleCell = cell.Module( 581 "example", 582 "Example module", 583 584 cell.Provide(New), 585 ) 586 587 type Example struct { /* ... */ } 588 func (e *Example) Start(ctx HookContext) error { /* ... */ } 589 func (e *Example) Stop(ctx HookContext) error { /* ... */ } 590 591 func New(lc cell.Lifecycle) *Example { 592 e := &Example{} 593 lc.Append(e) 594 return e 595 } 596 597 These hooks are executed when hive.Run() is called. The HookContext given to 598 these hooks is there to allow graceful aborting of the starting or stopping, 599 either due to user pressing ``Control-C`` or due to a timeout. By default Hive has 600 5 minute start timeout and 1 minute stop timeout, but these are configurable 601 with SetTimeouts(). A grace time of 5 seconds is given on top of the timeout 602 after which the application is forcefully terminated, regardless of whether 603 the hook has finished or not. 604 605 .. _api_shutdowner: 606 607 Shutdowner 608 ^^^^^^^^^^ 609 610 Sometimes there's nothing else to do but crash. If a fatal error is encountered 611 in a ``Start()`` hook it's easy: just return the error and abort the start. After 612 starting one can initiate a shutdown using the ``hive.Shutdowner``: 613 614 .. code-block:: go 615 616 package hive 617 618 type Shutdowner interface { 619 Shutdown(...ShutdownOption) 620 } 621 622 func ShutdownWithError(err error) ShutdownOption { /* ... */ } 623 624 package example 625 626 type Example struct { 627 /* ... */ 628 Shutdowner hive.Shutdowner 629 } 630 631 func (e *Example) eventLoop() { 632 for { 633 /* ... */ 634 if err != nil { 635 // Uh oh, this is really bad, we've got to crash. 636 e.Shutdowner.Shutdown(hive.ShutdownWithError(err)) 637 } 638 } 639 } 640 641 Creating and running a hive 642 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 643 644 A hive is created using ``hive.New()``: 645 646 .. code-block:: go 647 648 // func New(cells ...cell.Cell) *Hive 649 var myHive = hive.New(FooCell, BarCell) 650 651 ``New()`` creates a new hive and registers all providers to it. Invoke 652 functions are not yet executed as our application may have multiple hives 653 and we need to delay object instantiation to until we know which hive to use. 654 655 However ``New`` does execute an invoke function to gather all command-line flags from 656 all configuration cells. These can be then registered with a Cobra command: 657 658 .. code-block:: go 659 660 var cmd *cobra.Command = /* ... */ 661 myHive.RegisterFlags(cmd.Flags()) 662 663 After that the hive can be started with ``myHive.Run()``. 664 665 Run() will first construct the parsed configurations and will then execute 666 all invoke functions to instantiate all needed objects. As part of this the 667 lifecycle hooks will have been appended (in dependency order). After that 668 the start hooks can be executed one after the other to start the hive. Once 669 started, Run() waits for SIGTERM and SIGINT signals and upon receiving one 670 will execute the stop hooks in reverse order to bring the hive down. 671 672 Now would be a good time to try this out in practice. You'll find a small example 673 application in `pkg/hive/example <https://github.com/cilium/cilium/tree/main/pkg/hive/example>`_. 674 Try running it with ``go run .`` and exploring the implementation (try what happens if a provider is commented out!). 675 676 Inspecting a hive 677 ~~~~~~~~~~~~~~~~~ 678 679 The ``hive.Hive`` can be inspected with the 'hive' command after it's 680 been registered with cobra: 681 682 .. code-block:: go 683 684 var rootCmd *cobra.Command = /* ... */ 685 rootCmd.AddCommand(myHive.Command()) 686 687 .. code-block:: shell-session 688 689 cilium$ go run ./daemon hive 690 Cells: 691 692 Ⓜ️ agent (Cilium Agent): 693 Ⓜ️ infra (Infrastructure): 694 Ⓜ️ k8s-client (Kubernetes Client): 695 ⚙️ (client.Config) { 696 K8sAPIServer: (string) "", 697 K8sKubeConfigPath: (string) "", 698 K8sClientQPS: (float32) 0, 699 K8sClientBurst: (int) 0, 700 K8sHeartbeatTimeout: (time.Duration) 30s, 701 EnableK8sAPIDiscovery: (bool) false 702 } 703 704 🚧 client.newClientset (cell.go:109): 705 ⇨ client.Config, cell.Lifecycle, logrus.FieldLogger 706 ⇦ client.Clientset 707 ... 708 709 Start hooks: 710 711 • gops.registerGopsHooks.func1 (cell.go:44) 712 • cmd.newDatapath.func1 (daemon_main.go:1625) 713 ... 714 715 Stop hooks: 716 ... 717 718 719 The hive command prints out the cells, showing what modules, providers, 720 configurations etc. exist and what they're requiring and providing. 721 Finally the command prints out all registered start and stop hooks. 722 Note that these hooks often depend on the configuration (e.g. k8s-client 723 will not insert a hook unless e.g. --k8s-kubeconfig-path is given). The 724 hive command takes the same command-line flags as the root command. 725 726 The provider dependencies in a hive can also be visualized as a graphviz dot-graph: 727 728 .. code-block:: bash 729 730 cilium$ go run ./daemon hive dot-graph | dot -Tx11 731 732 Guidelines 733 ~~~~~~~~~~ 734 735 Few guidelines one should strive to follow when implementing larger cells: 736 737 * A constructor function should only do validation and allocation. Spawning 738 of goroutines or I/O operations must not be performed from constructors, 739 but rather via the Start hook. This is required as we want to inspect the 740 object graph (e.g. ``hive.PrintObjects``) and side-effectful constructors would 741 cause undesired effects. 742 743 * Stop functions should make sure to block until all resources 744 (goroutines, file handles, …) created by the module have been cleaned 745 up (with e.g. ``sync.WaitGroup``). This makes sure that independent 746 tests in the same test suite are not affecting each other. Use 747 `goleak <https://github.com/uber-go/goleak>`_ to check that goroutines 748 are not leaked. 749 750 * Preferably each non-trivial cell would come with a test that validates that 751 it implements its public API correctly. The test also serves 752 as an example of how the cell's API is used and it also validates the 753 correctness of the cells it depends on which helps with refactoring. 754 755 * Utility cells should not Invoke(). Since cells may be used in many 756 applications it makes sense to make them lazy to allow bundling useful 757 utilities into one collection. If a utility cell has an invoke, it may be 758 instantiated even if it is never used. 759 760 * For large cells, provide interfaces and not struct pointers. A cell 761 can be thought of providing a service to the rest of the application. To 762 make it accessible, one should think about what APIs the module provides and 763 express these as well documented interface types. If the interface is large, 764 try breaking it up into multiple small ones. Interface types also allows 765 integration testing with mock implementations. The rational here is the same as 766 with "return structs, accept interfaces": since hive works with the names of types, 767 we want to "inject" interfaces into the object graph and not struct 768 pointers. Extra benefit is that separating the API implemented by a module 769 into one or more interfaces it is easier to document and easier to inspect 770 as all public method declarations are in one place. 771 772 * Use parameter (cell.In) and result (cell.Out) objects liberally. If a 773 constructor takes more than two parameters, consider using a parameter 774 struct instead. 775 776 Internals: Dependency injection with reflection 777 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 778 779 Hive is built on top of `uber/dig <https://github.com/uber-go/dig>`_, a reflection based library for building 780 dependency injection frameworks. In dig, you create a container, add in your 781 constructors and then "invoke" to create objects: 782 783 .. code-block:: go 784 785 func NewA() (A, error) { /* ... */ } 786 func NewB() B { /* ... */ } 787 func NewC(A, B) (C, error) { /* ... */ } 788 func setupC(C) error 789 790 // Create a new container for our constructors. 791 c := dig.New(dig.DeferAcyclicVerification()) 792 793 // Add in the constructors. Order does not matter. 794 c.Provide(NewC) 795 c.Provide(NewB) 796 c.Provide(NewA) 797 798 // Invoke a function that can depend on any of the values supplied by the 799 // registered constructors. 800 // Since this depends on "C", dig will construct first A and B 801 // (as C depends on them), and then C. 802 c.Invoke(func(c *C) { 803 // Do something with C 804 }) 805 806 807 This is the basis on top of which Hive is built. Hive calls dig’s Provide() 808 for each of the constructors registered with cell.Provide and then calls 809 invoke functions to construct the needed objects. The results from the 810 constructors are cached, so each constructor is called only once. 811 812 ``uber/dig`` uses Go’s "reflect" package that provides access to the 813 type information of the provide and invoke functions. For example, the 814 `Provide <https://pkg.go.dev/go.uber.org/dig#Container.Provide>`_ method does 815 something akin to this under the hood: 816 817 .. code-block:: go 818 819 // 'constructor' has type "func(...) ..." 820 typ := reflect.TypeOf(constructor) 821 if typ.Kind() != reflect.Func { /* error */ } 822 823 in := make([]reflect.Type, 0, typ.NumIn()) 824 for i := 0; i < typ.NumIn(); i++ { 825 in[i] = typ.In(i) 826 } 827 828 out := make([]reflect.Type, 0, typ.NumOut()) 829 for i := 0; i < typ.NumOut(); i++ { 830 out[i] = typ.Out(i) 831 } 832 833 container.providers = append(container.providers, &provider{constructor, in, out}) 834 835 836 `Invoke <https://pkg.go.dev/go.uber.org/dig#Container.Invoke>`_ will similarly 837 reflect on the function value to find out what are the required inputs and 838 then find the required constructors for the input objects and recursively 839 their inputs. 840 841 While building this on reflection is flexible, the downside is that missing 842 dependencies lead to runtime errors. Luckily dig produces excellent errors and 843 suggests closely matching object types in case of typos. Due to the desire 844 to avoid these runtime errors the constructed hive should be as static 845 as possible, e.g. the set of constructors and invoke functions should be 846 determined at compile time and not be dependent on runtime configuration. This 847 way the hive can be validated once with a simple unit test (``daemon/cmd/cells_test.go``). 848 849 Cell showcase 850 ~~~~~~~~~~~~~ 851 852 Logging 853 ^^^^^^^ 854 855 Logging is provided to all cells by default with the ``logrus.FieldLogger`` interface type. The log lines will include the field ``subsys=<module id>``. 856 857 .. code-block:: go 858 859 cell.Module( 860 "example", 861 "log example module", 862 863 cell.Provide( 864 func(log logrus.FieldLogger) Example { 865 log.Info("Hello") // subsys=example message=Hello 866 return Example{log: log} 867 }, 868 ), 869 ) 870 871 Kubernetes client 872 ^^^^^^^^^^^^^^^^^ 873 874 The `client package <https://pkg.go.dev/github.com/cilium/cilium/pkg/k8s/client>`_ provides the ``Clientset`` API 875 that combines the different clientsets used by Cilium into one composite value. Also provides ``FakeClientCell`` 876 for writing integration tests for cells that interact with the K8s api-server. 877 878 .. code-block:: go 879 880 var Cell = cell.Provide(New) 881 882 func New(cs client.Clientset) Example { 883 return Example{cs: cs} 884 } 885 886 func (e Example) CreateIdentity(id *ciliumv2.CiliumIdentity) error { 887 return e.cs.CiliumV2().CiliumIdentities().Create(e.ctx, id, metav1.CreateOptions{}) 888 } 889 890 Resource and the store (see below) is the preferred way of accessing Kubernetes object 891 state to minimize traffic to the api-server. The Clientset should usually 892 only be used for creating and updating objects. 893 894 Kubernetes Resource and Store 895 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 896 897 While not a cell by itself, `pkg/k8s/resource <https://pkg.go.dev/github.com/cilium/cilium/pkg/k8s/resource>`_ 898 provides an useful abstraction for providing shared event-driven access 899 to Kubernetes objects. Implemented on top of the client-go informer, 900 ``workqueue`` and store to codify the suggested pattern for controllers in a 901 type-safe way. This shared abstraction provides a simpler API to write and 902 test against and allows central control over what data (and at what rate) 903 is pulled from the api-server and how it’s stored (in-memory or persisted). 904 905 The resources are usually made available centrally for the application, 906 e.g. in cilium-agent they’re provided from `pkg/k8s/resource.go <https://github.com/cilium/cilium/blob/main/daemon/k8s/resources.go>`_. 907 See also the runnable example in `pkg/k8s/resource/example <https://github.com/cilium/cilium/tree/main/pkg/k8s/resource/example>`_. 908 909 .. code-block:: go 910 911 import "github.com/cilium/cilium/pkg/k8s/resource" 912 913 var nodesCell = cell.Provide( 914 func(lc cell.Lifecycle, cs client.Clientset) resource.Resource[v1.Node] { 915 lw := utils.ListerWatcherFromTyped[*v1.NodeList](cs.CoreV1().Nodes()) 916 return resource.New[*v1.Node](lc, lw) 917 }, 918 ) 919 920 var Cell = cell.Module( 921 "resource-example", 922 "Example of how to use Resource", 923 924 nodesCell, 925 cell.Invoke(printNodeUpdates), 926 ) 927 928 func printNodeUpdates(nodes resource.Resource[*v1.Node]) { 929 // Store() returns a typed locally synced store of the objects. 930 // This call blocks until the store has been synchronized. 931 store, err := nodes.Store() 932 ... 933 obj, exists, err := store.Get("my-node") 934 ... 935 objs, err := store.List() 936 ... 937 938 // Events() returns a channel of object change events. Closes 939 // when 'ctx' is cancelled. 940 // type Event[T] struct { Kind Kind; Key Key; Object T; Done func(err error) } 941 for ev := range nodes.Events(ctx) { 942 switch ev.Kind { 943 case resource.Sync: 944 // The store has now synced with api-server and 945 // the set of observed upsert events forms a coherent 946 // snapshot. Usually some sort of garbage collection or 947 // reconciliation is performed. 948 case resource.Upsert: 949 fmt.Printf("Node %s has updated: %v\n", ev.Key, ev.Object) 950 case resource.Delete: 951 fmt.Printf("Node %s has been deleted\n", key) 952 } 953 // Each event must be marked as handled. If non-nil error 954 // is given, the processing for this key is retried later 955 // according to rate-limiting and retry policy. The built-in 956 // retrying is often used if we perform I/O operations (like API client 957 // calls) from the handler and retrying makes sense. It should not 958 // be used on parse errors and similar. 959 ev.Done(nil) 960 } 961 } 962 963 Job groups 964 ^^^^^^^^^^ 965 966 The `job package <https://pkg.go.dev/github.com/cilium/hive/job>`_ contains logic that 967 makes it easy to manage units of work that the package refers to as "jobs". These jobs are 968 scheduled as part of a job group. 969 970 Every job is a callback function provided by the user with additional logic which 971 differs slightly for each job type. The jobs and groups manage a lot of the boilerplate 972 surrounding lifecycle management. The callbacks are called from the job to perform the actual 973 work. 974 975 These jobs themselves come in several varieties. The ``OneShot`` job invokes its callback just once. 976 This job type can be used for initialization after cell startup, routines that run for the full lifecycle 977 of the cell, or for any other task you would normally use a plain goroutine for. 978 979 The ``Timer`` job invokes its callback periodically. This job type can be used for periodic tasks 980 such as synchronization or garbage collection. Timer jobs can also be externally triggered in 981 addition to the periodic invocations. 982 983 The ``Observer`` job invokes its callback for every message sent on a ``stream.Observable``. This job 984 type can be used to react to a data stream or events created by other cells. 985 986 Please take a look at ``pkg/hive/examples/jobs.go`` for an example of how to use the job package. 987