kubebuilder 是一个用来帮助用户快速实现 Kubernetes CRD Operator 的 SDK,由于其易用性,在 Kubernetes 社区中被广泛地使用。在其第一版设计中,由于生成的模板代码扩展性不佳的问题,导致了一些用户的抱怨,于是维护者推出了 Scaffolding 的第二版实现。

有关 Kubernetes CRD,kubebuilder 第一版 Scaffolding 的使用,以及 Scaffolding 代码背后的实现逻辑,请看前文利用 kubebuilder 优化 Kubernetes Operator 开发体验。本文主要介绍 kubebuilder Scaffolding 的新实现带来的开发体验的变化,以及对这一新实现的个人看法。

首先来看看新的 Scaffolding 实现的目录结构:

.
├── api
│ └── v1beta1
│ ├── frigate_types.go
│ ├── frigate_types_test.go
│ ├── groupversion_info.go
│ ├── suite_test.go
│ └── zz_generated.deepcopy.go
├── bin
│ └── manager
├── config
│ ├── certmanager
│ │ ├── certificate.yaml
│ │ ├── kustomization.yaml
│ │ └── kustomizeconfig.yaml
│ ├── crd
│ │ ├── kustomization.yaml
│ │ ├── kustomizeconfig.yaml
│ │ └── patches
│ │ └── webhook_in_frigate.yaml
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ ├── manager_image_patch.yaml
│ │ ├── manager_prometheus_metrics_patch.yaml
│ │ ├── manager_webhook_patch.yaml
│ │ └── webhookcainjection_patch.yaml
│ ├── manager
│ │ ├── kustomization.yaml
│ │ └── manager.yaml
│ ├── rbac
│ │ ├── auth_proxy_role_binding.yaml
│ │ ├── auth_proxy_role.yaml
│ │ ├── auth_proxy_service.yaml
│ │ ├── kustomization.yaml
│ │ └── role_binding.yaml
│ ├── samples
│ │ └── ship_v1beta1_frigate.yaml
│ └── webhook
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── service.yaml
├── controllers
│ ├── frigate_controller.go
│ └── suite_test.go
├── Dockerfile
├── go.mod
├── hack
│ └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT

v1 Scaffolding 的实现,与没有使用 kubebuilder 实现的 Controller 具有类似的目录结构。而 v2 Scaffolding 的实现,则是对其进行了简化。

不过其变化最大的地方,还是在 controller 自身的实现逻辑中。首先来看看新版的 manager 是怎样的:

var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)

func init() {

shipv1beta1.AddToScheme(scheme)
// +kubebuilder:scaffold:scheme
}

func main() {
var metricsAddr string
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.Parse()

ctrl.SetLogger(zap.Logger(true))

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme, MetricsBindAddress: metricsAddr})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}

err = (&controllers.FrigateReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Frigate"),
}).SetupWithManager(mgr)
if err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Frigate")
os.Exit(1)
}
// +kubebuilder:scaffold:builder

setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

在之前的实现中,controller 的创建是被放在 controller 自己的包中进行的,这就导致了,来自 CLI 的命令行参数很难被传递到 controller 中,控制 controller 的逻辑。同时也使得第一版的实现对不太熟悉 Kubernetes 内置的 controller(如 deployment controller 等)实现的开发者很难立刻入手,因为第一版的实现的目录结构是遵循了 Kubernetes 社区的一些惯例的。为了解决这些问题,第二版的实现利用了 Builder 模式简化了整个的过程。

在 Manager 的创建中,ctrl.NewManager 与第一版实现作用相同,都是创建了一个新的 Manager 实例。第二版的 Scheme 是事先被创建好,然后被传递进 Manager 中的。而在第一版中,是首先创建出 Manager 实例,然后再将 CR 添加到 Scheme 中的。不过这并无本质不同,在第二版中,将 CR 添加到 Scheme 中这一操作是在 main 包中进行的,这使得逻辑更加清晰。

接下来可以看到,controller 的创建被放在了 main 包中进行,这使得之前提到的问题都可以被避免,命令行参数可以被直接传递到 controller 中。而其后的 SetupWithManager 则是利用 Builder 模式创建出了完整的 controller 的实例。

接下来我们继续往下走,看下 controller 是如何真正被创建的。

// FrigateReconciler reconciles a Frigate object
type FrigateReconciler struct {
client.Client
Log logr.Logger
}

// +kubebuilder:rbac:groups=ship.example.com,resources=frigates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ship.example.com,resources=frigates/status,verbs=get;update;patch
func (r *FrigateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
_ = context.Background()
_ = r.Log.WithValues("frigate", req.NamespacedName)

// your logic here

return ctrl.Result{}, nil
}

func (r *FrigateReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&shipv1beta1.Frigate{}).
Complete(r)
}

SetupWithManager 是利用了 controller-runtime 中实现的 Controller Builder,构建了完整的 Controller 实例。

// Builder builds a Controller.
type Builder struct {
apiType runtime.Object
mgr manager.Manager
predicates []predicate.Predicate
managedObjects []runtime.Object
watchRequest []watchRequest
config *rest.Config
ctrl controller.Controller
}
// SimpleController returns a new Builder.
func SimpleController() *Builder {
return &Builder{}
}
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager
func ControllerManagedBy(m manager.Manager) *Builder {
return SimpleController().WithManager(m)
}
// WithManager sets the Manager to use for registering the ControllerManagedBy. Defaults to a new manager.Manager.
func (blder *Builder) WithManager(m manager.Manager) *Builder {
blder.mgr = m
return blder
}
// For defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete /
// update events by *reconciling the object*.
// This is the equivalent of calling
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{})
// If the passed in object has implemented the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
// If the passed in object has implemented the admission.Validator interface, a ValidatingWebhook will be wired for this type.
func (blder *Builder) For(apiType runtime.Object) *Builder {
blder.apiType = apiType
return blder
}

ControllerManagedBy 返回了一个被设置好 Manager 的 Builder。随后的 For 是给 Builder 设置了要 watch 的资源类型。

// Complete builds the Application ControllerManagedBy.
func (blder *Builder) Complete(r reconcile.Reconciler) error {
_, err := blder.Build(r)
return err
}
// Build builds the Application ControllerManagedBy and returns the Manager used to start it.
func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
if r == nil {
return nil, fmt.Errorf("must provide a non-nil Reconciler")
}
// Set the Config
if err := blder.doConfig(); err != nil {
return nil, err
}
// Set the Manager
if err := blder.doManager(); err != nil {
return nil, err
}
// Set the ControllerManagedBy
if err := blder.doController(r); err != nil {
return nil, err
}
// Set the Webook if needed
if err := blder.doWebhook(); err != nil {
return nil, err
}
// Set the Watch
if err := blder.doWatch(); err != nil {
return nil, err
}
return blder.mgr, nil
}

最后,Builder 会将 Controller 构建成完整的实例,并且加入到 Manager 下,这一过程与第一版实现并无二致。

整体来看,第二版 Scaffolding 的实现无疑大大提高了易用性,与第一版相比,功能相同但具有更高的扩展性。其最大的变动,是利用了 Builder 模式,让整个构建过程变得更加灵活。用户可以根据自己的需要来在 Builder 中加入对应的构建过程。

欢迎关注东岳的 GitHub 以及博客 :)

License

  • This article is licensed under CC BY-NC-SA 3.0.
  • Please contact me for commercial use.

推荐阅读:

相关文章