kubebuilder创建crd分析

问题备忘:

编号 问题 解决
0 kubebuiler的代码对调用crd的过程进行了比较多的封装,比如controller包里的Reconcile代码,具体是哪里调用到的? 在controller里实现的Reconcile方法
1 关于函数createStructuredListWatch和结构体informersByGVK又是什么时候执行和实现的呢

正文:

关于kubebuilder的使用可以参考官方文档:
https://book.kubebuilder.io/quick-start.html

本文不对kubebuidler的使用做分析,主要对kubebuiler的生成的代码进行分析。kubebuiler的代码对调用crd的过程进行了比较多的封装,需要对内部逻辑更好的了解,才能更好的进行crd开发。
话不多说,直接看代码:

### main.go
var (
    scheme   = runtime.NewScheme()    // 定义出一个空的scheme结构体
    setupLog = ctrl.Log.WithName("setup")
)

func init() {
    fmt.Printf("get scheme0: %v", scheme)  // &{map[] map[] map[] map[] map[] map[] 0xc0000b2660 map[] [] pkg/runtime/scheme.go:101}
    _ = clientgoscheme.AddToScheme(scheme) // 注册client-go里的预定义的结构体
    fmt.Printf("get scheme1: %v", scheme)
    _ = appsv1alpha1.AddToScheme(scheme) // 注册本crd的scheme信息,eg: {apps.devops.xxx.com v1alpha1 WeApp}
    // 但是此时并没有注册crd的结构体信息
    fmt.Printf("get scheme2: %v", scheme)
    // +kubebuilder:scaffold:scheme
}

crd的gvk信息注册之后,进入main函数

### main.go
func main() {

    var metricsAddr string        // 定义metricsAddr
    var enableLeaderElection bool // 用于组件的高可用,部署多个副本,通过选主来保证只有一个副本运行。选主的原理就是通过获得ETCD中的分布式锁成为leader,其他副本不断的去抢占这个锁,leader挂掉就会被其他副本抢占。
  ...

////1、NewManager returns a new Manager for creating Controllers.
// 这里生成一个通用的manager, 此时还没有传入自定义controller的逻辑
// ctrl.GetConfigOrDie() 获取本地的kubeconfig配置,要是没有获取到就退出
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 
        Scheme:             scheme,
        MetricsBindAddress: metricsAddr,
        Port:               9443,
        LeaderElection:     enableLeaderElection,
        LeaderElectionID:   "8bf23ea1.xxx.xxx.com",  // 应该是分布式锁的id
    })

//// 2、SetupWithManager用于注册controller逻辑到manager(struct controllerManager),放在leaderElectionRunnables或者是nonLeaderElectionRunnables字段里面,用于后面start的时候启动[]Runnable
    if err = (&controllers.WeDoctorAppReconciler{
        Client: mgr.GetClient(),
        Log:    ctrl.Log.WithName("controllers").WithName("MyApp"),
        Scheme: mgr.GetScheme(),
    }).SetupWithManager(mgr); err != nil {
        setupLog.Error(err, "unable to create controller", "controller", "WeDoctorApp")
        os.Exit(1)
    }
    // +kubebuilder:scaffold:builder

//// 3、mgr.Start 启动mgr里的[]Runnable,最终调用到每个controller的start方法
// SetupSignalHandler设置退出信号
    setupLog.Info("starting manager")
    if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
        setupLog.Error(err, "problem running manager")
        os.Exit(1)
    }
}

拆分出来几个关键的步骤一一进行分析:

1、生成一个manager用于注册和启动controller
ctrl.NewManager()

2、注册controller到manager
(&controllers.WeDoctorAppReconciler{
        Client: mgr.GetClient(),
        Log:    ctrl.Log.WithName("controllers").WithName("WeDoctorApp"),
        Scheme: mgr.GetScheme(),
    }).SetupWithManager(mgr);

3、启动controller
mgr.Start(ctrl.SetupSignalHandler())

1、ctrl.NewManager()

// New returns a new Manager for creating Controllers.
func New(config *rest.Config, options Options) (Manager, error) {
    // Initialize a rest.config if none was specified
    if config == nil {
        return nil, fmt.Errorf("must specify Config")
    }
    // Set default values for options fields
    options = setOptionsDefaults(options)  // 设置各种默认的参数
    // Create the mapper provider
    mapper, err := options.MapperProvider(config)  // gvk,gvr等之间的转换map
    // Create the cache for the cached read client and registering informers
    cache, err := options.NewCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod, Namespace: options.Namespace})  
// cache主要是为了创建informers的map关系,对每个gvk都创建了相应的informer,通过 informersByGVK 这个 map 做 GVK 到 Informer 的映射,每个 Informer 会根据 ListWatch 函数对对应的 GVK 进行 List 和 Watch。

    apiReader, err := client.New(config, client.Options{Scheme: options.Scheme, Mapper: mapper})
// 读client
    writeObj, err := options.NewClient(cache, config, client.Options{Scheme: options.Scheme, Mapper: mapper})
// 写client

...

    return &controllerManager{
        config:                config,
        scheme:                options.Scheme,
        cache:                 cache,
        fieldIndexes:          cache,
        client:                writeObj,
        apiReader:             apiReader,
        recorderProvider:      recorderProvider,
        resourceLock:          resourceLock,
        mapper:                mapper,
        metricsListener:       metricsListener,
        internalStop:          stop,
        internalStopper:       stop,
        port:                  options.Port,
        host:                  options.Host,
        certDir:               options.CertDir,
        leaseDuration:         *options.LeaseDuration,
        renewDeadline:         *options.RenewDeadline,
        retryPeriod:           *options.RetryPeriod,
        healthProbeListener:   healthProbeListener,
        readinessEndpointName: options.ReadinessEndpointName,
        livenessEndpointName:  options.LivenessEndpointName,
    }, nil
}

NewCache的具体实现是在setOptionsDefaults中定义的:

// New initializes and returns a new Cache.
func New(config *rest.Config, opts Options) (Cache, error) {
    opts, err := defaultOpts(config, opts)
    if err != nil {
        return nil, err
    }
    im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace)
    return &informerCache{InformersMap: im}, nil
}

// NewInformersMap creates a new InformersMap that can create informers for
// both structured and unstructured objects.
func NewInformersMap(config *rest.Config,
    scheme *runtime.Scheme,
    mapper meta.RESTMapper,
    resync time.Duration,
    namespace string) *InformersMap {

    return &InformersMap{
        structured:   newStructuredInformersMap(config, scheme, mapper, resync, namespace),
        unstructured: newUnstructuredInformersMap(config, scheme, mapper, resync, namespace),

        Scheme: scheme,
    }
}

// newStructuredInformersMap creates a new InformersMap for structured objects.
func newStructuredInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, resync time.Duration, namespace string) *specificInformersMap {
    return newSpecificInformersMap(config, scheme, mapper, resync, namespace, createStructuredListWatch)
}

// newSpecificInformersMap returns a new specificInformersMap (like
// the generical InformersMap, except that it doesn't implement WaitForCacheSync).
func newSpecificInformersMap(config *rest.Config,
    scheme *runtime.Scheme,
    mapper meta.RESTMapper,
    resync time.Duration,
    namespace string,
    createListWatcher createListWatcherFunc) *specificInformersMap {
    ip := &specificInformersMap{
        config:            config,
        Scheme:            scheme,
        mapper:            mapper,
        informersByGVK:    make(map[schema.GroupVersionKind]*MapEntry),
        codecs:            serializer.NewCodecFactory(scheme),
        paramCodec:        runtime.NewParameterCodec(scheme),
        resync:            resync,
        startWait:         make(chan struct{}),
        createListWatcher: createListWatcher,
        namespace:         namespace,
    }
    return ip
}

// newListWatch returns a new ListWatch object that can be used to create a SharedIndexInformer.
func createStructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) {
    // Kubernetes APIs work against Resources, not GroupVersionKinds.  Map the
    // groupVersionKind to the Resource API we will use.
    mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
    if err != nil {
        return nil, err
    }

    client, err := apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs)
    if err != nil {
        return nil, err
    }
    listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List")
    listObj, err := ip.Scheme.New(listGVK)
    if err != nil {
        return nil, err
    }

    // Create a new ListWatch for the obj
    return &cache.ListWatch{
        ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
            res := listObj.DeepCopyObject()
            isNamespaceScoped := ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot
            err := client.Get().NamespaceIfScoped(ip.namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Do().Into(res)
            return res, err
        },
        // Setup the watch function
        WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
            // Watch needs to be set to true separately
            opts.Watch = true
            isNamespaceScoped := ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot
            return client.Get().NamespaceIfScoped(ip.namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Watch()
        },
    }, nil
}

NewCache就是为了定义出gvk到informer之间的映射关系。
问题:关于函数createStructuredListWatch和结构体informersByGVK又是什么时候执行和实现的呢???

2、SetupWithManager

SetupWithManager用于给manager注册controllers。

func (r *WeDoctorAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&appsv1alpha1.WeDoctorApp{}).
        Complete(r)
}

这里用到了builer模式,NewControllerManagedBy用于生成builer结构体,for参数用于传参,除了for还有其他的一些参数eg:owns(表明当前资源拥有哪些附属资源),最后就是complete的构建过程。

// 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
    ctrlOptions    controller.Options
    name           string
}

// Build builds the Application ControllerManagedBy and returns the Controller it created.
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
    // Set the ControllerManagedBy
    if err := blder.doController(r); err != nil {
        return nil, err
    }
    // Set the Watch
    if err := blder.doWatch(); err != nil {
        return nil, err
    }
    return blder.ctrl, nil
}

主要有两个方法blder.doController(r)和 blder.doWatch()
(1)blder.doController(r)
通过mgr.Add(c)把controller加入到manager的[]Runner里。

// New returns a new Controller registered with the Manager.  The Manager will ensure that shared Caches have
// been synced before the Controller is Started.
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
  ...
    // Create controller with dependencies set
    c := &controller.Controller{
        Do:       options.Reconciler,    // 传入了Do的
        Cache:    mgr.GetCache(),
        Config:   mgr.GetConfig(),
        Scheme:   mgr.GetScheme(),
        Client:   mgr.GetClient(),
        Recorder: mgr.GetEventRecorderFor(name),
        MakeQueue: func() workqueue.RateLimitingInterface {
            return workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), name)
        },
        MaxConcurrentReconciles: options.MaxConcurrentReconciles,
        Name:                    name,
    }
    // Add the controller as a Manager components
    return c, mgr.Add(c)   
}

// Start implements controller.Controller
func (c *Controller) Start(stop <-chan struct{}) error {
        // NB(directxman12): launch the sources *before* trying to wait for the
        // caches to sync so that they have a chance to register their intendeded
        // caches.
        for _, watch := range c.watches {
            log.Info("Starting EventSource", "controller", c.Name, "source", watch.src)
            if err := watch.src.Start(watch.handler, c.Queue, watch.predicates...); err != nil {
                return err
            }
        }
     ...
        // Launch workers to process resources
        log.Info("Starting workers", "controller", c.Name, "worker count", c.MaxConcurrentReconciles)
        for i := 0; i < c.MaxConcurrentReconciles; i++ {
            // Process work items
            go wait.Until(c.worker, c.JitterPeriod, stop)
        }
// 通过mgr.Start(ctrl.SetupSignalHandler())会调用到每个controller里的start方法,然后调用到c.Do.Reconcile(req),即为前面传入的options.Reconciler的Reconcile方法,options.Reconciler即为我们自己定义的WeDoctorAppReconciler
}

(2)blder.doWatch()
doWatch的逻辑就是把watch列表塞到c.watches中,最后在(c *Controller) Start方法里会去执行start方法进行watch的。

func (blder *Builder) doWatch() error {
    // Reconcile type
    src := &source.Kind{Type: blder.apiType}
    hdler := &handler.EnqueueRequestForObject{}

// 注册的 handler,可以看到 Kubebuidler 为我们注册的 Handler 就是将发生变更的对象的 NamespacedName 入队列,如果在 Reconcile 逻辑中需要判断创建/更新/删除,需要有自己的判断逻辑。

    err := blder.ctrl.Watch(src, hdler, blder.predicates...)
    if err != nil {
        return err
    }
    // Watches the managed types
    for _, obj := range blder.managedObjects {
        src := &source.Kind{Type: obj}
        hdler := &handler.EnqueueRequestForOwner{
            OwnerType:    blder.apiType,
            IsController: true,
        }
        if err := blder.ctrl.Watch(src, hdler, blder.predicates...); err != nil {
            return err
        }
    }
    // Do the watch requests
    for _, w := range blder.watchRequest {
        if err := blder.ctrl.Watch(w.src, w.eventhandler, blder.predicates...); err != nil {
            return err
        }
    }
    return nil
}

// Watch implements controller.Controller
func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
    c.mu.Lock()
    defer c.mu.Unlock()
...
    c.watches = append(c.watches, watchDescription{src: src, handler: evthdler, predicates: prct})
    if c.Started {        //最开始不是Started,所以不会去watch
        log.Info("Starting EventSource", "controller", c.Name, "source", src)
        return src.Start(evthdler, c.Queue, prct...)
    }
}

所以整体的逻辑就是外部写Reconciler的Reconcile方法,由内部的controller的watch方法来接收event事件,将事件塞入到queue队列里面。Controller的start方法就会去触发reconcileHandler,进一步调用到外部注入的Reconcile方法进行具体的逻辑处理。

3、mgr.Start(ctrl.SetupSignalHandler())

通过之前的分析,接下来的mgr.Start(ctrl.SetupSignalHandler())的逻辑还是比较简单的

func (cm *controllerManager) Start(stop <-chan struct{}) error {
    // join the passed-in stop channel as an upstream feeding into cm.internalStopper
    defer close(cm.internalStopper)

    // initialize this here so that we reset the signal channel state on every start
    cm.errSignal = &errSignaler{errSignal: make(chan struct{})}

    // Metrics should be served whether the controller is leader or not.
    // (If we don't serve metrics for non-leaders, prometheus will still scrape
    // the pod but will get a connection refused)
    if cm.metricsListener != nil {
        go cm.serveMetrics(cm.internalStop)
    }

    // Serve health probes
    if cm.healthProbeListener != nil {
        go cm.serveHealthProbes(cm.internalStop)
    }

    go cm.startNonLeaderElectionRunnables()

    if cm.resourceLock != nil {
        err := cm.startLeaderElection()
        if err != nil {
            return err
        }
    } else {
        go cm.startLeaderElectionRunnables()
    }

    select {
    case <-stop:
        // We are done
        return nil
    case <-cm.errSignal.GotError():
        // Error starting a controller
        return cm.errSignal.Error()
    }
}

就是执行controllerManager里面的runnables。

参考文章:
https://my.oschina.net/u/3874284/blog/3110372
https://blog.csdn.net/RA681t58CJxsgCkJ31/article/details/104386126/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349