sigs.k8s.io/kubebuilder/v3@v3.14.0/designs/simplified-scaffolding.md (about) 1 | Authors | Creation Date | Status | Extra | 2 |---------------|---------------|-------------|---| 3 | @DirectXMan12 | Mar 6, 2019 | Implemented | - | 4 5 Simplified Builder-Based Scaffolding 6 ==================================== 7 8 ## Background 9 10 The current scaffolding in kubebuilder produces a directory structure that 11 looks something like this (compiled artifacts like config omitted for 12 brevity): 13 14 <details> 15 16 <summary>`tree -d ./test/project`</summary> 17 18 ```shell 19 $ tree -d ./test/project 20 ./test/project 21 ├── cmd 22 │ └── manager 23 ├── pkg 24 │ ├── apis 25 │ │ ├── creatures 26 │ │ │ └── v2alpha1 27 │ │ ├── crew 28 │ │ │ └── v1 29 │ │ ├── policy 30 │ │ │ └── v1beta1 31 │ │ └── ship 32 │ │ └── v1beta1 33 │ ├── controller 34 │ │ ├── firstmate 35 │ │ ├── frigate 36 │ │ ├── healthcheckpolicy 37 │ │ ├── kraken 38 │ │ └── namespace 39 │ └── webhook 40 │ └── default_server 41 │ ├── firstmate 42 │ │ └── mutating 43 │ ├── frigate 44 │ │ └── validating 45 │ ├── kraken 46 │ │ └── validating 47 │ └── namespace 48 │ └── mutating 49 └── vendor 50 ``` 51 52 </details> 53 54 API packages have a separate file for each API group that creates a SchemeBuilder, 55 a separate file to aggregate those scheme builders together, plus files for types, 56 and the per-group-version scheme builders as well: 57 58 <details> 59 60 <summary>`tree ./test/project/pkg/apis`</summary> 61 62 ```shell 63 $ ./test/project/pkg/apis 64 ├── addtoscheme_creatures_v2alpha1.go 65 ├── apis.go 66 ├── creatures 67 │ ├── group.go 68 │ └── v2alpha1 69 │ ├── doc.go 70 │ ├── kraken_types.go 71 │ ├── kraken_types_test.go 72 │ ├── register.go 73 │ ├── v2alpha1_suite_test.go 74 │ └── zz_generated.deepcopy.go 75 ... 76 ``` 77 78 </details> 79 80 Controller packages have a separate file that registers each controller with a global list 81 of controllers, a file that provides functionality to register that list with a manager, 82 as well as a file that constructs the individual controller itself: 83 84 <details> 85 86 <summary>`tree ./test/project/pkg/controller`</summary> 87 88 ```shell 89 $ tree ./test/project/pkg/controller 90 ./test/project/pkg/controller 91 ├── add_firstmate.go 92 ├── controller.go 93 ├── firstmate 94 │ ├── firstmate_controller.go 95 │ ├── firstmate_controller_suite_test.go 96 │ └── firstmate_controller_test.go 97 ... 98 ``` 99 100 </details> 101 102 ## Motivation 103 104 The current scaffolding in Kubebuilder has two main problems: 105 comprehensibility and dependency passing. 106 107 ### Complicated Initial Structure 108 109 While the structure of Kubebuilder projects will likely feel at home for 110 existing Kubernetes contributors (since it matches the structure of 111 Kubernetes itself quite closely), it provides a fairly convoluted 112 experience out of the box. 113 114 Even for a single controller and API type (without a webhook), it 115 generates 8 API-related files and 5 controller-related files. Of those 116 files, 6 are Kubebuilder-specific glue code, 4 are test setup, and 117 1 contains standard Kubernetes glue code, leaving only 2 with actual 118 user-edited code. 119 120 This proliferation of files makes it difficult for users to understand how 121 their code relates to the library, posing some barrier for initial adoption 122 and moving beyond a basic knowledge of functionality to actual 123 understanding of the structure. A common line of questioning amongst 124 newcomers to Kubebuilder includes "where should I put my code that adds 125 new types to a scheme" (and similar questions), which indicates that it's 126 not immediately obvious to these users why the project is structured the 127 way it is. 128 129 Additionally, we scaffold out API "tests" that test that the API server is 130 able to receive create requests for the objects, but don't encourage 131 modification beyond that. An informal survey seems to indicate that most 132 users don't actually modify these tests (many repositories continue to 133 look like 134 [this](https://github.com/replicatedhq/gatekeeper/blob/3bfe0f7213b6d41abf2df2a6746f3351e709e6ff/pkg/apis/policies/v1alpha2/admissionpolicy_types_test.go)). 135 If we want to help users test that their object's structure is the way 136 they think it is, we're probably better served coming up with a standard 137 "can I create this example YAML file". 138 139 Furthermore, since the structure is quite convoluted, it makes it more 140 difficult to write examples, since the actual code we care about ends up 141 scattered deep in a folder structure. 142 143 ### Lack of Builder 144 145 We introduced the builder pattern for controller construction in 146 controller-runtime 147 ([GoDoc](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/builder?tab=doc#ControllerManagedBy)) 148 as a way to simplify construction of controllers and reduce boilerplate 149 for the common cases of controller construction. Informal feedback from 150 this has been positive, and it enables fairly rapid, clear, and concise 151 construction of controllers (e.g. this [one file 152 controller](https://github.com/DirectXMan12/sample-controller/blob/workshop/main.go) 153 used as a getting started example for a workshop). 154 155 Current Kubebuilder scaffolding does not take advantage of the builder, 156 leaving generated code using the lower-level constructs which require more 157 understanding of the internals of controller-runtime to comprehend. 158 159 ### Dependency Passing Woes 160 161 Another common line of questioning amongst Kubebuilder users is "how to 162 I pass dependencies to my controllers?". This ranges from "how to I pass 163 custom clients for the software I'm running" to "how to I pass 164 configuration from files and flags down to my controllers" (e.g. 165 [kubernete-sigs/kubebuilder#611](https://github.com/kubernetes-sigs/kubebuilder/issues/611) 166 167 Since reconciler implementations are initialized in `Add` methods with 168 standard signatures, dependencies cannot be passed directly to 169 reconcilers. This has lead to requests for dependency injection in 170 controller-runtime (e.g. 171 [kubernetes-sigs/controller-runtime#102](https://github.com/kubernetes-sigs/controller-runtime/issues/102)), 172 but in most cases, a structure more amicable to passing in the 173 dependencies directly would solve the issue (as noted in 174 [kubernetes-sigs/controller-runtime#182](https://github.com/kubernetes-sigs/controller-runtime/pull/182#issuecomment-442615175)). 175 176 ## Revised Structure 177 178 In the revised structure, we use the builder pattern to focus on the 179 "code-refactor-code-refactor" cycle: start out with a simple structure, 180 refactor out as your project becomes more complicated. 181 182 Users receive a simply scaffolded structure to start. Simple projects can 183 remain relatively simple, and complicated projects can decide to adopt 184 a different structure as they grow. 185 186 The new scaffold project structure looks something like this (compiled 187 artifacts like config omitted for brevity): 188 189 ```shell 190 $ tree ./test/project 191 ./test/project 192 ├── main.go 193 ├── controller 194 │ ├── mykind_controller.go 195 │ ├── mykind_controller_test.go 196 │ └── controllers_suite_test.go 197 ├── api 198 │ └── v1 199 │ └── mykind_types.go 200 │ └── groupversion_info.go 201 └── vendor 202 ``` 203 204 In this new layout, `main.go` constructs the reconciler: 205 206 ```go 207 // ... 208 func main() { 209 // ... 210 err := (&controllers.MyReconciler{ 211 MySuperSpecialAppClient: doSomeThingsWithFlags(), 212 }).SetupWithManager(mgr) 213 // ... 214 } 215 ``` 216 217 while `mykind_controller.go` actually sets up the controller using the 218 reconciler: 219 220 ```go 221 func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager) error { 222 return ctrl.NewControllerManagedBy(mgr). 223 For(&api.MyAppType{}). 224 Owns(&corev1.Pod{}). 225 Complete(r) 226 } 227 ``` 228 229 This makes it abundantly clear where to start looking at the code 230 (`main.go` is the defacto standard entry-point for many go programs), and 231 simplifies the levels of hierarchy. Furthermore, since `main.go` actually 232 instantiates an instance of the reconciler, users are able to add custom 233 logic having to do with flags. 234 235 Notice that we explicitly construct the reconciler in `main.go`, but put 236 the setup logic for the controller details in `mykind_controller.go`. This 237 makes testing easier (see 238 [below](#put-the-controller-setup-code-in-main-go)), but still allows us 239 to pass in dependencies from `main`. 240 241 ### Why don't we... 242 243 #### Put the controller setup code in main.go 244 245 While this is an attractive pattern from a prototyping perspective, it 246 makes it harder to write integration tests, since you can't easily say 247 "run this controller with all its setup in processes". With a separate 248 `SetupWithManager` method associated with reconcile, it becomes fairly 249 easy to setup with a manager. 250 251 #### Put the types directly under api/, or not have groupversion_info.go 252 253 These suggestions make it much harder to scaffold out additional versions 254 and kinds. You need to have each version in a separate package, so that 255 type names don't conflict. While we could put scheme registration in with 256 `kind_types.go`, if a project has multiple "significant" Kinds in an API 257 group, it's not immediately clear which file has the scheme registration. 258 259 #### Use a single types.go file 260 261 This works fine when you have a single "major" Kind, but quickly grows 262 unwieldy when you have multiple major kinds and end up with 263 a hundreds-of-lines-long `types.go` file (e.g. the `appsv1` API group in 264 core Kubernetes). Splitting out by "major" Kind (`Deployment`, 265 `ReplicaSet`, etc) makes the code organization clearer. 266 267 #### Change the current scaffold to just make Add a method on the reconciler 268 269 While this solves the dependency issues (mostly, since you might want to 270 further pass configuration to the setup logic and not just the runtime 271 logic), it does not solve the underlying pedagogical issues around the 272 initial structure burying key logic amidst a sprawl of generated files and 273 directories. 274 275 ### Making this work with multiple controllers, API versions, API groups, etc 276 277 #### Versions 278 279 Most projects will eventually grow multiple API versions. The only 280 wrinkle here is making sure API versions get added to a scheme. This can 281 be solved by adding a specially-marked init function that registration 282 functions get added to (see the example). 283 284 #### Groups 285 286 Some projects eventually grow multiple API groups. Presumably, in the 287 case of multiple API groups, the desired hierarchy is: 288 289 ```shell 290 $ tree ./test/project/api 291 ./test/project/api 292 ├── groupa 293 │ └── v1 294 │ └── types.go 295 └── groupb 296 └── v1 297 └── types.go 298 ``` 299 300 There are three options here: 301 302 1. Scaffold with the more complex API structure (this looks pretty close 303 to what we do today). It doesn't add a ton of complexity, but does 304 bury types deeper in a directory structure. 305 306 2. Try to move things and rename references. This takes a lot more effort 307 on the Kubebuilder maintainers' part if we try to rename references 308 across the codebase. Not so much if we force the user to, but that's 309 a poorer experience. 310 311 3. Tell users to move things, and scaffold out with the new structure. 312 This is fairly messy for the user. 313 314 Since growing to multiple API groups seems to be fairly uncommon, it's 315 mostly like safe to take a hybrid approach here -- allow manually 316 specifying the output path, and, when not specified, asking the user to 317 first restructure before running the command. 318 319 #### Controllers 320 321 Multiple controllers don't need their own package, but we'd want to 322 scaffold out the builder. We have two options here: 323 324 1. Looking for a particular code comment, and appending a new builder 325 after it. This is a bit more complicated for us, but perhaps provides 326 a nicer UX. 327 328 2. Simply adding a new controller, and reminding the user to add the 329 builder themselves. This is easier for the maintainers, but perhaps 330 a slightly poorer UX for the users. However, writing out a builder by 331 hand is significantly less complex than adding a controller by hand in 332 the current structure. 333 334 Option 1 should be fairly simple, since the logic is already needed for 335 registering types to the scheme, and we can always fall back to emitting 336 code for the user to place in manually if we can't find the correct 337 comment. 338 339 ### Making this work with Existing Kubebuilder Installations 340 341 Kubebuilder projects currently have a `PROJECT` file that can be used to 342 store information about project settings. We can make use of this to 343 store a "scaffolding version", where we increment versions when making 344 incompatible changes to how the scaffolding works. 345 346 A missing scaffolding version field implies the version `1`, which uses 347 our current scaffolding semantics. Version `2` uses the semantics 348 proposed here. New projects are scaffolded with `2`, and existing 349 projects check the scaffold version before attempting to add addition API 350 versions, controllers, etc 351 352 ### Teaching more complicated project structures 353 354 Some controllers may eventually want more complicated project structures. 355 We should have a section of the book recommending options for when you 356 project gets very complicated. 357 358 ### Additional Tooling Work 359 360 * Currently the `api/` package will need a `doc.go` file to make 361 `deepcopy-gen` happy. We should fix this. 362 363 * Currently, `controller-gen crd` needs the `api` directory to be 364 `pkg/apis/<group>/<version>`. We should fix this. 365 366 ## Example 367 368 See #000 for an example with multiple stages of code generation 369 (representing the examples is this form is rather complicated, since it 370 involves multiple files). 371 372 ```shell 373 $ kubebuilder init --domain test.k8s.io 374 $ kubebuilder create api --group mygroup --version v1beta1 --kind MyKind 375 $ kubebuilder create api --group mygroup --version v2beta1 --kind MyKind 376 $ tree . 377 . 378 ├── main.go 379 ├── controller 380 │ ├── mykind_controller.go 381 │ ├── controller_test.go 382 │ └── controllers_suite_test.go 383 ├── api 384 │ ├── v1beta1 385 │ │ ├── mykind_types.go 386 │ │ └── groupversion_info.go 387 │ └── v1 388 │ ├── mykind_types.go 389 │ └── groupversion_info.go 390 └── vendor 391 ``` 392 393 <details> 394 395 <summary>main.go</summary> 396 397 ```go 398 package main 399 400 import ( 401 "os" 402 403 ctrl "sigs.k8s.io/controller-runtime" 404 "sigs.k8s.io/controller-runtime/pkg/log/zap" 405 "k8s.io/apimachinery/pkg/runtime" 406 407 "my.repo/api/v1beta1" 408 "my.repo/api/v1" 409 "my.repo/controllers" 410 ) 411 412 var ( 413 scheme = runtime.NewScheme() 414 setupLog = ctrl.Log.WithName("setup") 415 ) 416 417 func init() { 418 v1beta1.AddToScheme(scheme) 419 v1.AddToScheme(scheme) 420 // +kubebuilder:scaffold:scheme 421 } 422 423 func main() { 424 ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 425 426 mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme}) 427 if err != nil { 428 setupLog.Error(err, "unable to start manager") 429 os.Exit(1) 430 } 431 432 err = (&controllers.MyKindReconciler{ 433 Client: mgr.GetClient(), 434 log: ctrl.Log.WithName("mykind-controller"), 435 }).SetupWithManager(mgr) 436 if err != nil { 437 setupLog.Error(err, "unable to create controller", "controller", "mykind") 438 os.Exit(1) 439 } 440 441 // +kubebuilder:scaffold:builder 442 443 setupLog.Info("starting manager") 444 if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 445 setupLog.Error(err, "problem running manager") 446 os.Exit(1) 447 } 448 } 449 ``` 450 451 </details> 452 453 <details> 454 455 <summary>mykind_controller.go</summary> 456 457 ```go 458 package controllers 459 460 import ( 461 "context" 462 463 ctrl "sigs.k8s.io/controller-runtime" 464 "sigs.k8s.io/controller-runtime/pkg/client" 465 "github.com/go-logr/logr" 466 467 "my.repo/api/v1" 468 ) 469 470 type MyKindReconciler struct { 471 client.Client 472 log logr.Logger 473 } 474 475 func (r *MyKindReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 476 ctx := context.Background() 477 log := r.log.WithValues("mykind", req.NamespacedName) 478 479 // your logic here 480 481 return req.Result{}, nil 482 } 483 484 func (r *MyKindReconciler) SetupWithManager(mgr ctrl.Manager) error { 485 return ctrl.NewControllerManagedBy(mgr). 486 For(v1.MyKind{}). 487 Complete(r) 488 } 489 ``` 490 491 </details> 492 493 `*_types.go` looks nearly identical to the current standard. 494 495 <details> 496 497 <summary>groupversion_info.go</summary> 498 499 ```go 500 package v1 501 502 import ( 503 "sigs.k8s.io/controller-runtime/pkg/scheme" 504 "k8s.io/apimachinery/pkg/runtime/schema" 505 ) 506 507 var ( 508 GroupVersion = schema.GroupVersion{Group: "mygroup.test.k8s.io", Version: "v1"} 509 510 // SchemeBuilder is used to add go types to the GroupVersionKind scheme 511 SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 512 513 // AddToScheme adds the types in this group-version to the given scheme. 514 AddToScheme = SchemeBuilder.AddToScheme 515 ) 516 ``` 517 518 </details>