3

概述

我正在为可以在集群中垂直扩展部署的 VerticalScaler CRD 编写 Kubernetes 控制器。我的规范引用了集群中现有的部署对象。如果修改或删除了引用的部署,我想将 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和 Kubebuilder 文档中寻找一种好的方法。我发现的最接近的例子是kubebuilder-v1 文档中关于 watch 的“Watching Arbitrary Resources”部分:

控制器可以监视任意资源并将它们映射到控制器管理的资源的键。控制器甚至可以将一个事件映射到多个键,为每个键触发 Reconciles。

示例:为了响应集群扩展事件(例如删除或添加节点),控制器将监视节点并将监视事件映射到控制器管理的对象的键。

我的挑战是如何将部署映射到依赖的 VerticalScaler,因为部署中不存在此信息。我可以在 VerticalScaler 上创建一个索引,并使用字段选择器从MapFunc中查找依赖的 VerticalScalers ,但我似乎不应该在 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 watcher,但文档说这是针对源自集群外部的事件,但事实并非如此。

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

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

4

2 回答 2

1

您确实使用了地图功能和普通手表。https://github.com/coderanger/migrations-operator/blob/088a3b832f0acab4bfe02c03a4404628c5ddfd97/components/migrations.go#L64-L91显示了一个示例。你最终确实经常不得不在 map 函数中执行 I/O 来确定这个东西对应于哪个根对象,但我同意如果这些调用除了记录或恐慌之外没有其他方法可以做,这有点糟糕失败。

您还可以使用非控制器所有者引用或注释作为存储给定部署的映射目标的一种方式,这使得映射功能更加简单,但通常响应性也较低。总体而言,这取决于这需要多么动态。随时在#kubebuilder Slack 频道上寻求帮助。

于 2021-05-03T02:31:16.447 回答
0

作为 的替代方法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 操作的错误。

于 2022-02-23T22:04:49.517 回答