func (cw *CRWatcher) Watch(stopCh <-chan struct{}) error {
	if cw.controller == nil {
		return errors.New("the CRWatcher has not been initialized")
	}
	cw.controller.Run(stopCh)
	return nil
}
Watch will be called to begin watching the configured custom resource. All
events will be passed back to the ResourceController
package crwatcher
import (
	"errors"
	"time"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/dynamic"
	restclient "k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
)
type Config struct {
	Group      string         
	Namespace  string         
	Version    string         
	PluralName string         
	Filter     string         
	Resync     time.Duration  
}
type CRWatcher struct {
	Config     *Config
	resource   dynamic.ResourceInterface
	handler    cache.ResourceEventHandlerFuncs
	store      cache.Store
	controller cache.Controller
	logger     ErrorLogger
}
type ResourceController interface {
	ResourceAdded(resource *unstructured.Unstructured)
	ResourceUpdated(oldResource, newResource *unstructured.Unstructured)
	ResourceDeleted(resource *unstructured.Unstructured)
}
type ErrorLogger interface {
	Error(err error)
}
func NewCRWatcher(cfg *Config, kubeCfg *restclient.Config, rc ResourceController, l ErrorLogger) (*CRWatcher, error) {
	cw := &CRWatcher{
		Config: cfg,
		logger: l,
	}
	kubeCfg.ContentConfig.GroupVersion = &schema.GroupVersion{
		Group:   cfg.Group,
		Version: cfg.Version,
	}
	kubeCfg.APIPath = "apis"
	dynClient, err := dynamic.NewClient(kubeCfg)
	if err != nil {
		return nil, err
	}
	dc := dynClient
	cw.setupResource(dc)
	cw.setupHandler(rc)
	cw.setupController()
	cw.setupRuntimeLogging()
	return cw, nil
}
func (cw *CRWatcher) setupRuntimeLogging() {
	if cw.logger != nil {
		utilruntime.ErrorHandlers = []func(error){
			cw.logKubeError,
		}
	}
}
func (cw *CRWatcher) logKubeError(err error) {
	cw.logger.Error(err)
}
func (cw *CRWatcher) setupHandler(con ResourceController) {
	cw.handler = cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			r := obj.(*unstructured.Unstructured)
			if cw.passesFiltering(r) {
				con.ResourceAdded(r)
			}
		},
		DeleteFunc: func(obj interface{}) {
			r := obj.(*unstructured.Unstructured)
			if cw.passesFiltering(r) {
				con.ResourceDeleted(r)
			}
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			oldR := oldObj.(*unstructured.Unstructured)
			newR := newObj.(*unstructured.Unstructured)
			cw.update(con, oldR, newR)
		},
	}
}
func (cw *CRWatcher) update(con ResourceController, oldR *unstructured.Unstructured, newR *unstructured.Unstructured) {
	if cw.passesFiltering(newR) {
		if cw.passesFiltering(oldR) {
			con.ResourceUpdated(oldR, newR)
			return
		}
		con.ResourceAdded(newR)
	} else if cw.passesFiltering(oldR) {
		con.ResourceDeleted(oldR)
	}
}
func (cw *CRWatcher) setupResource(dc *dynamic.Client) {
	apiResource := &metav1.APIResource{
		Name:       cw.Config.PluralName,
		Namespaced: cw.Config.Namespace != metav1.NamespaceNone,
	}
	cw.resource = dc.Resource(apiResource, cw.Config.Namespace)
}
func (cw *CRWatcher) setupController() {
	listFunc := func(opts metav1.ListOptions) (runtime.Object, error) {
		return cw.resource.List(opts)
	}
	watchFunc := func(opts metav1.ListOptions) (watch.Interface, error) {
		return cw.resource.Watch(opts)
	}
	lw := &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc}
	cw.store, cw.controller = cache.NewInformer(
		lw,
		&unstructured.Unstructured{},
		cw.Config.Resync,
		cw.handler,
	)
}
func (cw *CRWatcher) passesFiltering(r *unstructured.Unstructured) bool {
	if cw.Config.Filter == "" {
		return true
	}
	annotations := r.GetAnnotations()
	if annotations == nil {
		return false
	}
	_, ok := annotations[cw.Config.Filter]
	return ok
}