func(r*ApplicationReconciler)generateDeployment(appv1.Application)appsv1.Deployment{deploy:=appsv1.Deployment{ObjectMeta:metav1.ObjectMeta{Name:deploymentName(app.Name),Namespace:app.Namespace,Labels:map[string]string{"app":app.Name,},},Spec:appsv1.DeploymentSpec{Replicas:ptr.To(int32(1)),// 副本数Selector:&metav1.LabelSelector{MatchLabels:map[string]string{"app":app.Name,},},Template:corev1.PodTemplateSpec{ObjectMeta:metav1.ObjectMeta{Labels:map[string]string{"app":app.Name,},},Spec:corev1.PodSpec{Containers:[]corev1.Container{{Name:app.Name,Image:app.Spec.Image,// Ports: []corev1.ContainerPort{// {// ContainerPort: 80,// },// },},},},},},}// Set the ownerRef for the Deployment, ensuring that the Deployment// will be deleted when the Application CR is deleted._=controllerutil.SetControllerReference(&app,&deploy,r.Scheme)returndeploy}
// SetupWithManager sets up the controller with the Manager.func(r*ApplicationReconciler)SetupWithManager(mgrctrl.Manager)error{returnctrl.NewControllerManagedBy(mgr).For(&v1.Application{}).Watches(&appsv1.Deployment{},handler.EnqueueRequestsFromMapFunc(func(ctxcontext.Context,objclient.Object)[]ctrl.Request{app,ok:=obj.GetLabels()["app"]if!ok{// if no app label,means not owned by app,do nothingreturnnil}return[]ctrl.Request{{NamespacedName:types.NamespacedName{Namespace:obj.GetNamespace(),Name:app,// return name is app name,not deployment name},}}})).Named("application").Complete(r)}
// SetupWithManager sets up the controller with the Manager.func(r*ApplicationReconciler)SetupWithManager(mgrctrl.Manager)error{returnctrl.NewControllerManagedBy(mgr).For(&v1.Application{}).Owns(&appsv1.Deployment{}).Named("application").Complete(r)}
配置子资源权限
为了 Watch 子资源,以触发调谐,需要给 Controller 赋予足够的权限。
在对于位置增加 Marker 赋予 Deployment CRUD 权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications,verbs=get;list;watch;create;update;patch;delete// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/status,verbs=get;update;patch// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/finalizers,verbs=update// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete// Reconcile is part of the main kubernetes reconciliation loop which aims to// move the current state of the cluster closer to the desired state.// TODO(user): Modify the Reconcile function to compare the state specified by// the Application object against the actual cluster state, and then// perform operations to make the cluster state reflect the state specified by// the user.//
// For more details, check Reconcile and its Result here:// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcilefunc(r*ApplicationReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){}
ifapp.ObjectMeta.DeletionTimestamp.IsZero(){// The object is not being deleted, so if it does not have our finalizer,// then lets add the finalizer and update the object.if!sets.NewString(app.ObjectMeta.Finalizers...).Has(AppFinalizer){log.Log.Info("new app,add finalizer","app",req.NamespacedName)app.ObjectMeta.Finalizers=append(app.ObjectMeta.Finalizers,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{log.Log.Error(err,"unable to add finalizer to application","app",req.NamespacedName)returnctrl.Result{},err}}}else{// The object is being deleted// if our finalizer is present, handle deletion// if not present, maybe clean up was already done,do nothingifsets.NewString(app.ObjectMeta.Finalizers...).Has(AppFinalizer){log.Log.Info("app deleted, clean up","app",req.NamespacedName)// do clean up// remove our finalizer from the list and update it.app.ObjectMeta.Finalizers=sets.NewString(app.ObjectMeta.Finalizers...).Delete(AppFinalizer).UnsortedList()iferr=r.Update(ctx,&app);err!=nil{log.Log.Error(err,"unable to delete finalizer","app",req.NamespacedName)returnctrl.Result{},err}}// if no finalizer,do nothingreturnctrl.Result{},nil}
func(r*ApplicationReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){logger:=log.FromContext(ctx)log:=logger.WithValues("application",req.NamespacedName)log.Info("start reconcile")// query appvarappv1.Applicationerr:=r.Get(ctx,req.NamespacedName,&app)iferr!=nil{log.Error(err,"unable to fetch application")// we'll ignore not-found errors, since they can't be fixed by an immediate// requeue (we'll need to wait for a new notification), and we can get them// on deleted requests.returnctrl.Result{},client.IgnoreNotFound(err)}// examine DeletionTimestamp to determine if object is under deletionifapp.ObjectMeta.DeletionTimestamp.IsZero(){// The object is not being deleted, so if it does not have our finalizer,// then lets add the finalizer and update the object. This is equivalent// to registering our finalizer.if!controllerutil.ContainsFinalizer(&app,AppFinalizer){controllerutil.AddFinalizer(&app,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{log.Error(err,"unable to add finalizer to application")returnctrl.Result{},err}}}else{// The object is being deletedifcontrollerutil.ContainsFinalizer(&app,AppFinalizer){// our finalizer is present, so lets handle any external dependencyiferr=r.deleteExternalResources(&app);err!=nil{log.Error(err,"unable to cleanup application")// if fail to delete the external dependency here, return with error// so that it can be retried.returnctrl.Result{},err}// remove our finalizer from the list and update it.controllerutil.RemoveFinalizer(&app,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{returnctrl.Result{},err}}// Stop reconciliation as the item is being deletedreturnctrl.Result{},nil}// Your reconcile logicreturnctrl.Result{},nil}func(r*ApplicationReconciler)deleteExternalResources(app*v1.Application)error{//
// delete any external resources associated with the cronJob//
// Ensure that delete implementation is idempotent and safe to invoke// multiple times for same object.returnnil}
varappv1.Applicationerr:=r.Get(ctx,req.NamespacedName,&app)iferr!=nil{log.Error(err,"unable to fetch application")// we'll ignore not-found errors, since they can't be fixed by an immediate// requeue (we'll need to wait for a new notification), and we can get them// on deleted requests.returnctrl.Result{},client.IgnoreNotFound(err)}
// examine DeletionTimestamp to determine if object is under deletionifapp.ObjectMeta.DeletionTimestamp.IsZero(){// The object is not being deleted, so if it does not have our finalizer,// then lets add the finalizer and update the object. This is equivalent// to registering our finalizer.if!controllerutil.ContainsFinalizer(&app,AppFinalizer){controllerutil.AddFinalizer(&app,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{log.Error(err,"unable to add finalizer to application")returnctrl.Result{},err}}}else{// The object is being deletedifcontrollerutil.ContainsFinalizer(&app,AppFinalizer){// our finalizer is present, so lets handle any external dependencyiferr=r.deleteExternalResources(&app);err!=nil{log.Error(err,"unable to cleanup application")// if fail to delete the external dependency here, return with error// so that it can be retried.returnctrl.Result{},err}// remove our finalizer from the list and update it.controllerutil.RemoveFinalizer(&app,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{returnctrl.Result{},err}}// Stop reconciliation as the item is being deletedreturnctrl.Result{},nil}
注意点
不使用 Finalizer 时,资源被删除无法获取任何信息;
对象的 Status 字段变化也会触发 Reconcile 方法;
Reconcile 逻辑需要幂等;
对象的 Status 字段变化也会触发 Reconcile 方法
为了防止触发无意义的 Reconcile,我们需要在更新 status 前判断是否需要更新,就像这样:
1
2
3
4
5
6
7
8
9
10
copyApp:=app.DeepCopy()// now,if ready replicas is gt 1,set status to truecopyApp.Status.Ready=deploy.Status.ReadyReplicas>=1if!reflect.DeepEqual(app,copyApp){// update when changedlog.Log.Info("sync app status","app",req.NamespacedName)iferr=r.Client.Status().Update(ctx,copyApp);err!=nil{log.Log.Error(err,"unable to update application status","app",req.NamespacedName)returnctrl.Result{},err}}
// ApplicationReconciler reconciles a Application objecttypeApplicationReconcilerstruct{client.ClientScheme*runtime.Scheme// See that we added the following code to allow us to pass the record.EventRecorderRecorderrecord.EventRecorder}
并在启动时初始化该字段
1
2
3
4
5
6
7
8
9
iferr=(&controller.ApplicationReconciler{Client:mgr.GetClient(),Scheme:mgr.GetScheme(),// Note that we added the following line:Recorder:mgr.GetEventRecorderFor("application-controller"),}).SetupWithManager(mgr);err!=nil{setupLog.Error(err,"unable to create controller","controller","Application")os.Exit(1)}
ifapp.ObjectMeta.DeletionTimestamp.IsZero(){// The object is not being deleted, so if it does not have our finalizer,// then lets add the finalizer and update the object. This is equivalent// to registering our finalizer.if!controllerutil.ContainsFinalizer(&app,AppFinalizer){controllerutil.AddFinalizer(&app,AppFinalizer)iferr=r.Update(ctx,&app);err!=nil{log.Error(err,"unable to add finalizer to application")returnctrl.Result{},err}// .. 添加// finalizer 时记录一个事件r.Recorder.Eventf(&app,corev1.EventTypeNormal,"AddFinalizer",fmt.Sprintf("add finalizer %s",AppFinalizer))}}else{// The object is being deletedifcontrollerutil.ContainsFinalizer(&app,AppFinalizer){// .. // 移除 finalizer 时记录一个事件r.Recorder.Eventf(&app,corev1.EventTypeNormal,"RemoveFinalizer",fmt.Sprintf("remove finalizer %s",AppFinalizer))}if!reflect.DeepEqual(app,copyApp){// update when changedlog.Info("app changed,update app status")iferr=r.Client.Status().Update(ctx,copyApp);err!=nil{log.Error(err,"unable to update application status")returnctrl.Result{},err}// 状态变化时也记录一个r.Recorder.Eventf(&app,corev1.EventTypeNormal,"UpdateStatus",fmt.Sprintf("update status from %v to %v",app.Status,copyApp.Status))}
// SetupWithManager sets up the controller with the Manager.func(r*ApplicationReconciler)SetupWithManager(mgrctrl.Manager)error{returnctrl.NewControllerManagedBy(mgr).For(&v1.Application{}).Watches(&appsv1.Deployment{},handler.EnqueueRequestsFromMapFunc(func(ctxcontext.Context,objclient.Object)[]ctrl.Request{app,ok:=obj.GetLabels()["app"]if!ok{// if no app label,means not owned by app,do nothingreturnnil}return[]ctrl.Request{{NamespacedName:types.NamespacedName{Namespace:obj.GetNamespace(),Name:app,// return name is app name,not deployment name},}}})).Named("application").Complete(r)}
packagecontrollerimport(v1"github.com/lixd/i-operator/api/v1""sigs.k8s.io/controller-runtime/pkg/event""sigs.k8s.io/controller-runtime/pkg/predicate")// Predicate to trigger reconciliation only on size changes in the Busybox specvarupdatePred=predicate.Funcs{// Only allow updates when the spec.image or spec.enabled of the Application resource changesUpdateFunc:func(eevent.UpdateEvent)bool{oldObj:=e.ObjectOld.(*v1.Application)newObj:=e.ObjectNew.(*v1.Application)// Trigger reconciliation only if the spec.size field has changedreturnoldObj.Spec.Image!=newObj.Spec.Image||oldObj.Spec.Enabled!=newObj.Spec.Enabled},// Allow create eventsCreateFunc:func(eevent.CreateEvent)bool{returntrue},// Allow delete eventsDeleteFunc:func(eevent.DeleteEvent)bool{returntrue},// Allow generic events (e.g., external triggers)GenericFunc:func(eevent.GenericEvent)bool{returntrue},}
核心逻辑:
1
2
3
4
5
6
7
UpdateFunc:func(eevent.UpdateEvent)bool{oldObj:=e.ObjectOld.(*v1.Application)newObj:=e.ObjectNew.(*v1.Application)// Trigger reconciliation only if the spec.size field has changedreturnoldObj.Spec.Image!=newObj.Spec.Image||oldObj.Spec.Enabled!=newObj.Spec.Enabled},
// SetupWithManager sets up the controller with the Manager.func(r*ApplicationReconciler)SetupWithManager(mgrctrl.Manager)error{returnctrl.NewControllerManagedBy(mgr).For(&v1.Application{}).Watches(&appsv1.Deployment{},handler.EnqueueRequestsFromMapFunc(func(ctxcontext.Context,objclient.Object)[]ctrl.Request{app,ok:=obj.GetLabels()["app"]if!ok{// if no app label,means not owned by app,do nothingreturnnil}return[]ctrl.Request{{NamespacedName:types.NamespacedName{Namespace:obj.GetNamespace(),Name:app,// return name is app name,not deployment name},}}}),builder.WithPredicates(updatePred)).Named("application").Complete(r)}
// +kubebuilder:object:root=true// +kubebuilder:subresource:status// +kubebuilder:resource:shortName="app"// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=`.spec.image`// +kubebuilder:printcolumn:name="Enabled",type=boolean,JSONPath=`.spec.enabled`// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.ready`// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"// Application is the Schema for the applications API.typeApplicationstruct{metav1.TypeMeta`json:",inline"`metav1.ObjectMeta`json:"metadata,omitempty"`SpecApplicationSpec`json:"spec,omitempty"`StatusApplicationStatus`json:"status,omitempty"`}
// +kubebuilder:object:root=true// +kubebuilder:subresource:status// +kubebuilder:resource:shortName="app"// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=`.spec.image`// +kubebuilder:printcolumn:name="Enabled",type=boolean,JSONPath=`.spec.enabled`// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.ready`// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"// Application is the Schema for the applications API.typeApplicationstruct{metav1.TypeMeta`json:",inline"`metav1.ObjectMeta`json:"metadata,omitempty"`SpecApplicationSpec`json:"spec,omitempty"`StatusApplicationStatus`json:"status,omitempty"`}
// +kubebuilder:object:root=true// +kubebuilder:subresource:status// +kubebuilder:resource:shortName="app"// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=`.spec.image`// +kubebuilder:printcolumn:name="Enabled",type=boolean,JSONPath=`.spec.enabled`// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.ready`// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"// Application is the Schema for the applications API.typeApplicationstruct{metav1.TypeMeta`json:",inline"`metav1.ObjectMeta`json:"metadata,omitempty"`SpecApplicationSpec`json:"spec,omitempty"`StatusApplicationStatus`json:"status,omitempty"`}
// +kubebuilder:webhook:path=/mutate-core-crd-lixueduan-com-v1-application,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.crd.lixueduan.com,resources=applications,verbs=create;update,versions=v1,name=mapplication-v1.lixueduan.com,admissionReviewVersions=v1typeApplicationCustomDefaulterstruct{// TODO(user): Add more fields as needed for defaulting}// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.// +kubebuilder:webhook:path=/validate-core-crd-lixueduan-com-v1-application,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.crd.lixueduan.com,resources=applications,verbs=create;update,versions=v1,name=vapplication-v1.lixueduan.com,admissionReviewVersions=v1typeApplicationCustomValidatorstruct{// TODO(user): Add more fields as needed for validation}
6. 其他
Controller Scope
设置 CRD Scope
在创建 API 时通过--namespaced 参数来指定时 namespace 还是 cluster 范围的 CRD,当不设置时,默认会当做 namespaced。
1
kubebuilder create api --group core --version v1 --kind Application --namespaced=true