sigs.k8s.io controller-runtime系列之一 builder分析

简介

之前介绍过controller-runtime总览sigs.k8s.io controller-runtime总览
本文主要介绍pkg/builder的源码分析。

目录结构

  1. builder_suite_test.go 校验k8s环境 获取client config

    • 依赖ginkgo做集成测试,表示该文件夹内的测试例执行之前执行,
    • BeforeSuite和AfterSuite,会在所有测试例执行之前和之后执行
    • 如果BeforeSuite执行失败,则这个测试集都不会被执行
  2. controller.go

    • Builder结构体 构建controller所需要的参数
    type Builder struct {
    forInput         ForInput //封装实现了runtime.Object的结构体
    ownsInput        []OwnsInput // 封装了apiType的owner,用于EnqueueRequestForOwner使用
    watchesInput     []WatchesInput // 封装type和evenHandler,一般使用owns和For代替,如果想扩展,可以使用
    mgr              manager.Manager // mgr  主要使用内部的参数构造controller
    globalPredicates []predicate.Predicate // 全局过滤事件
    ctrl             controller.Controller // 构造的controller
    ctrlOptions      controller.Options // 构造的controller 的参数包括最大并发reconcile数和reconciler
    name             string //默认是对应apiType的小写形式,符合普罗米修斯的命名格式
    }
    
    • ControllerManagedBy函数 根据提供的mgr生成一个新的builder
    func ControllerManagedBy(m manager.Manager) *Builder {
    	return &Builder{mgr: m}
    }
    
    • ForInput结构体 作为For方法的入参 (reconcile的object)
    type ForInput struct {
    object           client.Object //封装实现了runtime.Object的结构体
    predicates       []predicate.Predicate //过滤事件
    objectProjection objectProjection //标识我们可以发送/接收一个给定的resource(metadata-only, unstructured, etc)
    err              error
    }
    
    • blder For方法 新建forInput,设置forInput的predicates并给blder的forInput赋值
    func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
    	if blder.forInput.object != nil {
    		blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation")
    		return blder
    	}
    	input := ForInput{object: object}
    	for _, opt := range opts {
    		opt.ApplyToFor(&input)
    	}
    
    	blder.forInput = input
    	return blder
    }
    
    • OwnsInput结构体 作为Owns方法的入参 (ForInput的owner的object)
      type OwnsInput struct {
      	object           client.Object //封装实现了runtime.Object的结构体
      	predicates       []predicate.Predicate //过滤事件
      	objectProjection objectProjection //标识我们可以发送/接收一个给定的resource(metadata-only, unstructured, etc)
      }
      
    • blder Owns方法 给blder的ownsInput追加对象,具体看blder.ownsInput的作用
    func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
    	input := OwnsInput{object: object}
    	for _, opt := range opts {
    		opt.ApplyToOwns(&input)
    	}
    
    	blder.ownsInput = append(blder.ownsInput, input)
    	return blder
    }
    
    • watchRequest结构体 封装watch对象和事件处理器
    type watchRequest struct {
    	src          source.Source
    	eventhandler handler.EventHandler
        predicates       []predicate.Predicate
        objectProjection objectProjection
    }
    
    • blder Watches方法 给blder.watchesInput追加WatchesInput
    func (blder *Builder) Watches(src source.Source, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
    	input := WatchesInput{src: src, eventhandler: eventhandler}
    	for _, opt := range opts {
    		opt.ApplyToWatches(&input)
    	}
    
    	blder.watchesInput = append(blder.watchesInput, input)
    	return blder
    }
    
    • blder WithOptions方法 设置controller的Options
    • blder WithLogger方法 设置controller的Options.Log
    • blder WithEventFilter方法 给blder.globalPredicates追加过滤事件对象
    func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
    	blder.globalPredicates = append(blder.globalPredicates, p)
    	return blder
    }
    
    • blder Named方法 给blder.name赋值
    • blder Complete方法 注意了 ,这个是核心方法,根据提供的reconciler生成blder.ctrl开启ctrl.watch
        func (blder *Builder) Complete(r reconcile.Reconciler) error {
            //调用Build
        	_, err := blder.Build(r)
        	return err
        }
        
        func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
        	if r == nil {
        		return nil, fmt.Errorf("must provide a non-nil Reconciler")
        	}
        	if blder.mgr == nil {
        		return nil, fmt.Errorf("must provide a non-nil Manager")
        	}
        	if blder.forInput.err != nil {
        		return nil, blder.forInput.err
        	}
        	// 校验要reconcile的对象是否存在
        	if blder.forInput.object == nil {
        		return nil, fmt.Errorf("must provide an object for reconciliation")
        	}
        
        	// 创建controller赋值给blder.ctrl
        	if err := blder.doController(r); err != nil {
        		return nil, err
        	}
        
        	// 开启ctrl.watch(包括owner和apiType)
        	if err := blder.doWatch(); err != nil {
        		return nil, err
        	}
        
        	return blder.ctrl, nil
        }
    
        func (blder *Builder) doWatch() error {
        	// Reconcile 对应的apitype
        	typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
        	if err != nil {
        		return err
        	}
        	src := &source.Kind{Type: typeForSrc}
        	hdler := &handler.EnqueueRequestForObject{}
        	allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
        	if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
        		return err
        	}
        
        	// 监听owns对应的type
        	for _, own := range blder.ownsInput {
        		typeForSrc, err := blder.project(own.object, own.objectProjection)
        		if err != nil {
        			return err
        		}
        		src := &source.Kind{Type: typeForSrc}
        		hdler := &handler.EnqueueRequestForOwner{
        			OwnerType:    blder.forInput.object,
        			IsController: true,
        		}
        		allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
        		allPredicates = append(allPredicates, own.predicates...)
        		if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
        			return err
        		}
        	}
        
        	// 监听watchs对应的source
        	for _, w := range blder.watchesInput {
        		allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
        		allPredicates = append(allPredicates, w.predicates...)
        
        		// If the source of this watch is of type *source.Kind, project it.
        		if srckind, ok := w.src.(*source.Kind); ok {
        			typeForSrc, err := blder.project(srckind.Type, w.objectProjection)
        			if err != nil {
        				return err
        			}
        			srckind.Type = typeForSrc
        		}
        
        		if err := blder.ctrl.Watch(w.src, w.eventhandler, allPredicates...); err != nil {
        			return err
        		}
        	}
        	return nil
        }
    
        func (blder *Builder) doController(r reconcile.Reconciler) error {
            // 获取全局的opts
        	globalOpts := blder.mgr.GetControllerOptions()
        
            // 获取controller的opts
        	ctrlOptions := blder.ctrlOptions
            // 设置reconciler
        	if ctrlOptions.Reconciler == nil {
        		ctrlOptions.Reconciler = r
        	}
        
        	// 重新获取我们将要 reconciling的GVK
        	gvk, err := getGvk(blder.forInput.object, blder.mgr.GetScheme())
        	if err != nil {
        		return err
        	}
        
        	// 设置最大并发Reconcile的数目
        	if ctrlOptions.MaxConcurrentReconciles == 0 {
        		groupKind := gvk.GroupKind().String()
                // 如果获取当前的reconcile数大于0,就设置为当前的
        		if concurrency, ok := globalOpts.GroupKindConcurrency[groupKind]; ok && concurrency > 0 {
        			ctrlOptions.MaxConcurrentReconciles = concurrency
        		}
        	}
        
        	// 设置cache同步过期时间
        	if ctrlOptions.CacheSyncTimeout == 0 && globalOpts.CacheSyncTimeout != nil {
        		ctrlOptions.CacheSyncTimeout = *globalOpts.CacheSyncTimeout
        	}
        
        	// 设置controller的log
        	if ctrlOptions.Log == nil {
        		ctrlOptions.Log = blder.mgr.GetLogger()
        	}
        	ctrlOptions.Log = ctrlOptions.Log.WithValues("reconciler group", gvk.Group, "reconciler kind", gvk.Kind)
        
        	//构造controller  调用pkg/controller的New方法
        	blder.ctrl, err = newController(blder.getControllerName(gvk), blder.mgr, ctrlOptions)
        	return err
        }
    
  3. controller_test.go 测试操作builder的文件

  4. doc.go 提供一个log

  5. example_test.go 基于ReplicaSet的测试例子

  6. example_webhook_test.go 此示例使用Webhook构建器创建一个简单的Webhook,该Webhook由CRD ChaosPod的mgr进行管理。

  7. options.go 为controller提供设置Option的文件(设置Predicates和objectProjection)

  8. webhook.go

    • WebhookBuilder结构体 构建webhook所需要的参数
    type WebhookBuilder struct {
    	apiType runtime.Object // 钩子触发的apiType(实现了runtime.Object的结构体)
    	gvk     schema.GroupVersionKind // 钩子触发的apiType的gvk
    	mgr     manager.Manager // mgr提供构建webhook所需的属性
    	config  *rest.Config // k8s的client配置
    }
    
    • WebhookManagedBy函数 根据提供的mgr生成一个新的webhookBuilder
    func WebhookManagedBy(m manager.Manager) *WebhookBuilder {
      return &WebhookBuilder{mgr: m}
    }
    
    • blder For方法 给blder的apiType赋值
    func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
      blder.apiType = apiType
      return blder
    }
    
    • blder Complete方法 注意了 ,这个是核心方法,为apiType对应的GVK注册DefaultingWebhook(改变)、ValidatingWebhook(验证)和ConversionWebhook(version转换)
    func (blder *WebhookBuilder) Complete() error {
    	// 加载client config
    	blder.loadRestConfig()
    
    	// 注册需要的webhook
    	return blder.registerWebhooks()
    }
    
    func (blder *WebhookBuilder) registerWebhooks() error {
    	// 获取gvk 
    	var err error
    	blder.gvk, err = apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme())
    	if err != nil {
    		return err
    	}
    
        // 注册mutaing webhook
    	blder.registerDefaultingWebhook()
    	
    	// 注册validate webhook
    	blder.registerValidatingWebhook()
    
        // 注册conversion webhook
    	err = blder.registerConversionWebhook()
    	if err != nil {
    		return err
    	}
    	return nil
    }
    
    func (blder *WebhookBuilder) registerDefaultingWebhook() {
        //类型转换
    	defaulter, isDefaulter := blder.apiType.(admission.Defaulter)
    	if !isDefaulter {
    		log.Info("skip registering a mutating webhook, admission.Defaulter interface is not implemented", "GVK", blder.gvk)
    		return
    	}
    	//根据default validate使用pkg/admission获取mutaing webhook
    	mwh := admission.DefaultingWebhookFor(defaulter)
    	if mwh != nil {
    	    // 生成mutate path
    		path := generateMutatePath(blder.gvk)
    
    		// 校验该path是否已经注册
    		// 如果true,跳过
    		if !blder.isAlreadyHandled(path) {
    			log.Info("Registering a mutating webhook",
    				"GVK", blder.gvk,
    				"path", path)
    			blder.mgr.GetWebhookServer().Register(path, mwh)
    		}
    	}
    }
    
    func (blder *WebhookBuilder) registerValidatingWebhook() {
    	validator, isValidator := blder.apiType.(admission.Validator)
    	if !isValidator {
    		log.Info("skip registering a validating webhook, admission.Validator interface is not implemented", "GVK", blder.gvk)
    		return
    	}
    	vwh := admission.ValidatingWebhookFor(validator)
    	if vwh != nil {
    		path := generateValidatePath(blder.gvk)
    
    		// Checking if the path is already registered.
    		// If so, just skip it.
    		if !blder.isAlreadyHandled(path) {
    			log.Info("Registering a validating webhook",
    				"GVK", blder.gvk,
    				"path", path)
    			blder.mgr.GetWebhookServer().Register(path, vwh)
    		}
    	}
    }
    
    func (blder *WebhookBuilder) registerConversionWebhook() error {
    	ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
    	if err != nil {
    		log.Error(err, "conversion check failed", "object", blder.apiType)
    		return err
    	}
    	if ok {
    		if !blder.isAlreadyHandled("/convert") {
    			blder.mgr.GetWebhookServer().Register("/convert", &conversion.Webhook{})
    		}
    		log.Info("conversion webhook enabled", "object", blder.apiType)
    	}
    
    	return nil
    }
    
    //判断path是否已经注册过
    func (blder *WebhookBuilder) isAlreadyHandled(path string) bool {
    	if blder.mgr.GetWebhookServer().WebhookMux == nil {
    		return false
    	}
    	h, p := blder.mgr.GetWebhookServer().WebhookMux.Handler(&http.Request{URL: &url.URL{Path: path}})
    	if p == path && h != nil {
    		return true
    	}
    	return false
    }
    
    //生成mutate path
    func generateMutatePath(gvk schema.GroupVersionKind) string {
    	return "/mutate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" +
    		gvk.Version + "-" + strings.ToLower(gvk.Kind)
    }
    
    //生成validate path
    func generateValidatePath(gvk schema.GroupVersionKind) string {
    	return "/validate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" +
    		gvk.Version + "-" + strings.ToLower(gvk.Kind)
    }
    
  9. webhook_test.go 钩子测试文件

2025-04-02T15:18:10+08:00 ERROR controller-runtime.source.EventHandler if kind is a CRD, it should be installed before calling Start {"kind": "ImageSyncJob.middleware.zeroplus.com", "error": "no matches for kind \"ImageSyncJob\" in version \"middleware.zeroplus.com/v1\""} sigs.k8s.io/controller-runtime/pkg/internal/source.(*Kind).Start.func1.1 /home/admin/aone-build/go-mod-cache/sigs.k8s.io/controller-runtime@v0.16.3/pkg/internal/source/kind.go:63 k8s.io/apimachinery/pkg/util/wait.loopConditionUntilContext.func1 /home/admin/aone-build/go-mod-cache/k8s.io/apimachinery@v0.29.0/pkg/util/wait/loop.go:53 k8s.io/apimachinery/pkg/util/wait.loopConditionUntilContext /home/admin/aone-build/go-mod-cache/k8s.io/apimachinery@v0.29.0/pkg/util/wait/loop.go:54 k8s.io/apimachinery/pkg/util/wait.PollUntilContextCancel /home/admin/aone-build/go-mod-cache/k8s.io/apimachinery@v0.29.0/pkg/util/wait/poll.go:33 sigs.k8s.io/controller-runtime/pkg/internal/source.(*Kind).Start.func1 /home/admin/aone-build/go-mod-cache/sigs.k8s.io/controller-runtime@v0.16.3/pkg/internal/source/kind.go:56 2025-04-02T15:18:10+08:00 INFO Starting workers {"controller": "mqinitjob", "controllerGroup": "middleware.zeroplus.com", "controllerKind": "MqInitJob", "worker count": 1} 2025-04-02T15:18:10+08:00 INFO Starting workers {"controller": "configurationinitjob", "controllerGroup": "middleware.zeroplus.com", "controllerKind": "ConfigurationInitJob", "worker count": 1} 2025-04-02T15:18:10+08:00 INFO Starting workers {"controller": "application", "controllerGroup": "oam.zeroplus.com", "controllerKind": "Application", "worker count": 1} 2025-04-02T15:18:10+08:00 INFO Starting workers {"controller": "edasinitjob", "controllerGroup": "middleware.zeroplus.com", "controllerKind": "EdasInitJob", "worker count": 1} 2025-04-02T15:18:10+08:00 INFO Starting workers {"controller": "databaseinitjob", "controllerGroup": "middleware.zeroplus.com", "controllerKind": "DatabaseInitJob", "worker count": 1}
最新发布
04-03
<think>好的,我现在需要帮助用户解决他们遇到的Kubernetes控制器错误。首先,用户提供的日志显示了一个错误信息:“no matches for kind \"ImageSyncJob\" in version \"middleware.zeroplus.com/v1\"”。这表明控制器在尝试处理ImageSyncJob这个自定义资源定义(CRD)时,发现该CRD尚未在集群中注册。 接下来,我应该回顾Kubernetes控制器的工作原理。控制器通常通过监听特定资源类型的事件来工作。如果对应的CRD不存在,控制器在启动时无法找到该资源类型,就会报这样的错误。因此,用户的问题可能出在CRD的安装顺序上,即控制器启动时,CRD还没有被正确安装到集群中。 然后,我需要考虑可能的原因。首先,CRD可能确实没有被创建。用户需要检查是否已经应用了ImageSyncJob的CRD YAML文件。其次,CRD的版本是否正确,这里错误信息提到的是middleware.zeroplus.com/v1,所以需要确认CRD中指定的版本是否匹配。另外,有时候即使CRD已经存在,但可能因为API服务器尚未完成注册,控制器启动时仍然检测不到,这种情况可能需要确保控制器在CRD完全可用之后启动。 解决方案方面,第一步是确认CRD是否已正确安装。用户可以使用kubectl get crd命令查看是否存在ImageSyncJob.middleware.zeroplus.com。如果不存在,需要应用对应的CRD定义。如果存在,需要检查CRD的版本和组是否正确。此外,控制器部署可能需要调整依赖关系,确保CRD在控制器启动之前就绪。例如,在Helm Chart中,可以使用依赖项或者钩子来确保安装顺序。 另外,可能还需要检查控制器的日志是否有其他相关错误,或者API服务器的状态是否正常。有时候网络问题或权限问题也可能导致控制器无法访问CRD,但根据错误信息,更可能的是CRD未安装或版本不匹配。 最后,用户提供的日志中还有其他控制器成功启动的信息,比如MqInitJob、ConfigurationInitJob等,这可能意味着这些对应的CRD已经正确安装,而ImageSyncJob的CRD是唯一缺失的。因此,问题可能仅存在于ImageSyncJob这个特定的CRD上,需要单独处理。</think>你遇到的错误信息表明 Kubernetes 控制器在尝试监听 `ImageSyncJob` 资源时,发现该自定义资源定义(CRD)尚未在集群中注册。以下是逐步分析和解决方案: --- ### **错误原因分析** 1. **CRD 未注册** 错误日志 `no matches for kind "ImageSyncJob" in version "middleware.zeroplus.com/v1"` 表明: - Kubernetes API 服务器无法找到名为 `ImageSyncJob` 的 CRD。 - 该 CRD 应该属于 API 组 `middleware.zeroplus.com`,版本 `v1`。 2. **控制器启动顺序问题** Kubernetes 控制器需要在其管理的 CRD **已成功注册到集群**后才能启动。如果控制器在 CRD 就绪前启动,就会报此错误。 --- ### **解决方案** #### **步骤 1:确认 CRD 是否存在** 检查集群中是否已安装 `ImageSyncJob` 的 CRD: ```bash kubectl get crd | grep "imagesyncjobs.middleware.zeroplus.com" ``` - **如果存在**:检查 CRD 的 `apiVersion` 是否与控制器要求的 `middleware.zeroplus.com/v1` 一致。 - **如果不存在**:需要手动安装 CRD。 --- #### **步骤 2:安装缺失的 CRD** 假设 CRD 定义文件为 `ImageSyncJob-CRD.yaml`,内容示例: ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: imagesyncjobs.middleware.zeroplus.com spec: group: middleware.zeroplus.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object # 补充完整的 CRD 结构 scope: Namespaced names: plural: imagesyncjobs singular: imagesyncjob kind: ImageSyncJob shortNames: - isj ``` 应用 CRD: ```bash kubectl apply -f ImageSyncJob-CRD.yaml ``` --- #### **步骤 3:调整控制器启动顺序** 确保控制器在 CRD **完全就绪**后启动。常见方法: 1. **Helm Chart 依赖** 如果使用 Helm,在 `Chart.yaml` 中添加 CRD 为依赖: ```yaml dependencies: - name: image-sync-job-crd version: "1.0.0" repository: "https://your-chart-repo" ``` 2. **Init Container 等待** 在控制器 Pod 的 `initContainers` 中添加检查逻辑: ```yaml initContainers: - name: wait-for-crd image: bitnami/kubectl command: - sh - -c - "until kubectl get crd imagesyncjobs.middleware.zeroplus.com; do echo 'Waiting for CRD'; sleep 5; done" ``` --- #### **步骤 4:重启控制器** 更新控制器部署,确保其重新启动并加载最新的 CRD: ```bash kubectl rollout restart deploy <controller-deployment-name> ``` --- ### **补充验证** 1. **检查 CRD 状态** ```bash kubectl get crd imagesyncjobs.middleware.zeroplus.com -o yaml ``` 确认 `conditions` 中的 `Established` 状态为 `True`。 2. **查看控制器日志** ```bash kubectl logs -f <controller-pod-name> ``` 确保错误 `no matches for kind` 已消失。 --- ### **总结** 根本原因是控制器在 CRD 就绪前启动了。通过以下步骤修复: 1. **安装缺失的 CRD** 2. **确保控制器启动顺序依赖 CRD** 3. **重启控制器** 若仍有问题,请检查 CRD 的 YAML 定义是否与控制器代码中预期的 `Group-Version-Kind` 完全匹配。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值