如何为任意对象触发 Kubernetes 控制器协调器?

How to trigger a Kubernetes controller reconciler for an arbitrary object?

概览

我正在为 VerticalScaler CRD 编写一个 Kubernetes 控制器,它可以垂直扩展集群中的 Deployment。我的规范引用了集群中现有的 Deployment 对象。如果引用的 Deployment 被修改或删除,我想对 VerticalScaler 的协调请求进行排队。

// VerticalScalerSpec defines the desired state of VerticalScaler.
type VerticalScalerSpec struct {
    // Name of the Deployment object which will be auto-scaled.
    DeploymentName string `json:"deploymentName"`
}

问题

当控制器不拥有该资源并且该资源不持有对其资源由控制器管理的对象的引用时,是否有监视任意资源的好方法?

我发现了什么

我认为这应该在控制器的 Kubebuilder 标准 SetupWithManager 函数中配置,尽管可以在其他地方设置手表。

// SetupWithManager sets up the controller with the Manager.
func (r *VerticalScalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&v1beta1.VerticalScaler{}).
        Complete(r)
}

我一直在 controller-runtime/pkg/builder and the Kubebuilder docs. The closest example I found was the section "Watching Arbitrary Resources" in the kubebuilder-v1 docs on watches 中寻找好的方法:

Controllers may watch arbitrary Resources and map them to a key of the Resource managed by the controller. Controllers may even map an event to multiple keys, triggering Reconciles for each key.

Example: To respond to cluster scaling events (e.g. the deletion or addition of Nodes), a Controller would watch Nodes and map the watch events to keys of objects managed by the controller.

我的挑战是如何将 Deployment 映射到依赖的 VerticalScaler,因为 Deployment 上不存在此信息。我可以 create an index on the VerticalScaler and look up depending VerticalScalers from the MapFunc 使用字段选择器,但我似乎不应该在 MapFunc 中执行 I/O 。如果 list-Deployments 操作失败,我将无法重试或重新排队更改。

我有这段代码使用这种不完美的方法工作:

const deploymentNameIndexField = ".metadata.deploymentName"

// SetupWithManager sets up the controller with the Manager.
func (r *VerticalScalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
    if err := r.createIndices(mgr); err != nil {
        return err
    }

    return ctrl.NewControllerManagedBy(mgr).
        For(&v1beta1.VerticalScaler{}).
        Watches(
            &source.Kind{Type: &appsv1.Deployment{}},
            handler.EnqueueRequestsFromMapFunc(r.mapDeploymentToRequests)).
        Complete(r)
}

func (r *VerticalScalerReconciler) createIndices(mgr ctrl.Manager) error {
    return mgr.GetFieldIndexer().IndexField(
        context.Background(),
        &v1beta1.VerticalScaler{},
        deploymentNameIndexField,
        func(object client.Object) []string {
            vs := object.(*v1beta1.VerticalScaler)

            if vs.Spec.DeploymentName == "" {
                return nil
            }

            return []string{vs.Spec.DeploymentName}
        })
}

func (r *VerticalScalerReconciler) mapDeploymentToRequests(object client.Object) []reconcile.Request {
    deployment := object.(*appsv1.Deployment)

    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
    defer cancel()

    var vsList v1beta1.VerticalScalerList

    if err := r.List(ctx, &vsList,
        client.InNamespace(deployment.Namespace),
        client.MatchingFields{deploymentNameIndexField: deployment.Name},
    ); err != nil {
        r.Log.Error(err, "could not list VerticalScalers. " +
            "change to Deployment %s.%s will not be reconciled.",
            deployment.Name, deployment.Namespace)
        return nil
    }

    requests := make([]reconcile.Request, len(vsList.Items))

    for i, vs := range vsList.Items {
        requests[i] = reconcile.Request{
            NamespacedName: client.ObjectKeyFromObject(&vs),
        }
    }

    return requests
}

其他考虑过的方法

为了说明我的基本情况,我应该提到我不想将 VerticalScaler 设置为 Deployment 的所有者,因为我不想在删除 VerticalScaler 时对 Deployment 进行垃圾回收。即使是非控制器 ownerReference 也会导致垃圾收集。

我也考虑过使用 Channel 观察者,但文档说那是针对来自集群外部的事件,而事实并非如此。

我还可以为 Deployment 创建一个单独的控制器,并从该控制器的 Reconcile 函数更新依赖的 VerticalScaler 上的一些字段,但是我还需要一个终结器来处理在 Deployment 时触发 VerticalScaler 协调被删除了,这似乎有点过分了。

我可以让我的 VerticalScaler 协调器向 Deployment 添加注释,但如果由 Helm 等管理,Deployment 注释有可能被覆盖。在部署之前创建 VerticalScaler 的情况下,这也不会导致协调请求。

你确实用了地图功能和普通手表。 https://github.com/coderanger/migrations-operator/blob/088a3b832f0acab4bfe02c03a4404628c5ddfd97/components/migrations.go#L64-L91 显示了一个示例。你确实经常不得不在 map 函数中执行 I/O 来计算出这个东西对应于哪个根对象,但我同意这有点糟糕,除了 log 或 panic if 之外别无他法这些调用失败了。

您还可以使用非控制器所有者引用或注释作为一种方式来存储给定部署的映射目标,这使得映射功能更加简单,但通常响应速度较慢。总的来说,这取决于这需要多动态。欢迎随时访问#kubebuilder Slack 频道寻求帮助。

作为 EnqueueRequestsFromMapFunc 的替代方法,您可以使用:

ctrl.NewControllerManagedBy(mgr).
    For(&v1beta1.VerticalScaler{}).
    Watches(
        &source.Kind{Type: &appsv1.Deployment{}},
        handler.Funcs{CreateFunc: r.CreateFunc})...

处理程序的回调函数(例如上面您定义的 CreateFunc)具有签名 func(event.CreateEvent, workqueue.RateLimitingInterface),使您可以直接访问工作队列。默认情况下,如果您不在工作队列上调用 Done() ,它将通过指数退避重新排队。这应该允许您处理 io 操作的错误。