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>