
Kubernetes 的 `StorageClass.volumeBindingMode` 有两个常见取值：`Immediate` 和 `WaitForFirstConsumer`。

`Immediate` 会在 PVC 创建后尽快完成 PV 绑定或动态 provisioning。`WaitForFirstConsumer` 则会把绑定或 provisioning 推迟到第一个使用该 PVC 的 Pod 被调度时，让 scheduler 可以同时考虑 Pod 的 node selector、affinity、taints、资源需求、拓扑约束和卷拓扑。Kubernetes 官方文档对这个字段的解释是：`volumeBindingMode` 控制卷绑定和动态 provisioning 发生的时机；对拓扑受限、不是所有节点都可访问的存储，`Immediate` 可能让 Pod 之后不可调度，而 `WaitForFirstConsumer` 会延迟到使用 PVC 的 Pod 出现后再处理。

这个机制放到 KubeVirt + CDI 里，会出现一个额外问题：DataVolume 的数据导入、上传、克隆不是由虚拟机 Pod 自己完成的，而是由 CDI 先创建 worker pod 把数据写入 PVC。如果 CDI worker pod 成为「第一个消费者」，PVC 就会按 worker pod 的调度结果绑定到某个节点或拓扑域；等真正的 KubeVirt VMI Pod 创建时，它可能需要被调度到另一个节点，最终虚拟机启动失败。

`HonorWaitForFirstConsumer` 的作用就是避免这个问题：当 DataVolume 使用 `WaitForFirstConsumer` 存储，并且 PVC 还在 Pending 时，CDI 不主动创建 import/upload/clone worker pod；它把 DataVolume 标成 `WaitForFirstConsumer`，让 KubeVirt 先用符合 VMI 约束的临时 Pod 触发 PVC 绑定，然后 CDI 再继续数据填充。

<!--more-->

## 问题在哪里

CDI 官方文档已经把这个问题讲得很直接：CDI 会用 worker Pod 做 import/upload/clone；如果 worker Pod 触发了 WFFC PVC 的绑定，PVC 会绑定到 worker Pod 所在节点；但 worker Pod 的调度约束可能和 KubeVirt VM 不同，VM 调度到别的节点后，卷就不可用了。这个说明在 CDI 仓库的文档里也有永久链接：[doc/waitforfirstconsumer-storage-handling.md#L7-L14](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/doc/waitforfirstconsumer-storage-handling.md#L7-L14)。

这个场景典型发生在：

1. 本地盘、HostPath、local PV 这类节点绑定存储。
2. 单 AZ / 单 zone 块存储，卷一旦创建或绑定就有明确拓扑。
3. 多盘 VM，每块磁盘都由 CDI 管理，如果不同 PVC 被不同 worker pod 提前绑定，后续 VMI 很难同时满足所有卷约束。

所以这里不是单纯让 PVC 更晚绑定，而是保证「第一个真正影响 PVC 位置的消费者」尽量代表最终 workload，而不是代表数据导入任务。

## CDI 的特性开关

在 CDI 里，`HonorWaitForFirstConsumer` 是一个 feature gate：

```go
// HonorWaitForFirstConsumer - if enabled will not schedule worker pods on a storage with WaitForFirstConsumer binding mode
HonorWaitForFirstConsumer = "HonorWaitForFirstConsumer"
```

源码位置：[pkg/feature-gates/feature-gates.go#L16-L18](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/feature-gates/feature-gates.go#L16-L18)。

它的开关状态来自 `CDIConfig.Spec.FeatureGates`，`HonorWaitForFirstConsumerEnabled()` 最终只是检查配置里是否包含这个 feature gate：[pkg/feature-gates/feature-gates.go#L50-L75](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/feature-gates/feature-gates.go#L50-L75)。

不过全局开关不是唯一条件。CDI 还有一个 PVC/DataVolume 级别的绕过方式：`cdi.kubevirt.io/storage.bind.immediate.requested`。`IsWaitForFirstConsumerEnabled()` 的逻辑是：

```go
isImmediateBindingRequested := ImmediateBindingRequested(obj)
pvcHonorWaitForFirstConsumer := !isImmediateBindingRequested
globalHonorWaitForFirstConsumer, err := gates.HonorWaitForFirstConsumerEnabled()
return pvcHonorWaitForFirstConsumer && globalHonorWaitForFirstConsumer, nil
```

源码位置：[pkg/controller/common/util.go#L1543-L1554](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/common/util.go#L1543-L1554)。

也就是说，最终是否 honor WFFC，需要同时满足：

1. 全局 `HonorWaitForFirstConsumer` 开启。
2. 当前对象没有请求 immediate binding。

如果全局 feature gate 关闭，DataVolume controller 在生成 PVC 时还会主动补上 immediate binding annotation：[pkg/controller/common/util.go#L1556-L1565](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/common/util.go#L1556-L1565)。import、upload、clone 的 DataVolume controller 都会调用这个函数，例如 import 路径在 [pkg/controller/datavolume/import-controller.go#L120-L132](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/import-controller.go#L120-L132)，upload 路径在 [pkg/controller/datavolume/upload-controller.go#L116-L126](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/upload-controller.go#L116-L126)，clone 路径在 [pkg/controller/datavolume/clone-controller-base.go#L195-L205](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/clone-controller-base.go#L195-L205)。

## CDI 如何暂停 worker pod

CDI DataVolume controller 会检查 PVC 对应 StorageClass 的 binding mode：

```go
return storageClassBindingMode != nil &&
    *storageClassBindingMode == storagev1.VolumeBindingWaitForFirstConsumer
```

源码位置：[pkg/controller/datavolume/controller-base.go#L1240-L1248](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/controller-base.go#L1240-L1248)。

如果全局 feature gate 开启、StorageClass 是 WFFC、PVC 仍是 `Pending`，DataVolume 会被标记为 `WaitForFirstConsumer`：

```go
res := honorWaitForFirstConsumerEnabled && wffc &&
    pvc.Status.Phase == corev1.ClaimPending
```

源码位置：[pkg/controller/datavolume/controller-base.go#L1250-L1265](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/controller-base.go#L1250-L1265)，状态写入位置在 [pkg/controller/datavolume/controller-base.go#L1005-L1013](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/datavolume/controller-base.go#L1005-L1013)。`WaitForFirstConsumer` 也是 CDI API 里正式定义的 DataVolume phase：[staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go#L400-L404](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go#L400-L404)。

另一边，真正负责创建 worker pod 的 import/upload controller 会先调用 `IsWaitForFirstConsumerEnabled()`，然后进入统一的 `shouldHandlePvc()`：

```go
func shouldHandlePvc(pvc *corev1.PersistentVolumeClaim, honorWaitForFirstConsumerEnabled bool, log logr.Logger) bool {
    if honorWaitForFirstConsumerEnabled {
        return isBound(pvc, log)
    }
    return true
}
```

源码位置：[pkg/controller/util.go#L98-L105](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/util.go#L98-L105)。import controller 的调用在 [pkg/controller/import-controller.go#L177-L192](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/import-controller.go#L177-L192)，upload controller 的调用在 [pkg/controller/upload-controller.go#L172-L180](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/upload-controller.go#L172-L180)。

这段逻辑的效果很明确：

1. honor WFFC 时，只处理已经 `Bound` 的 PVC。
2. PVC 还在 `Pending` 时，CDI 不创建 worker pod。
3. 不 honor WFFC 时，CDI 可以照旧创建 worker pod，worker pod 会触发 PVC 绑定。

使用 CSI volume populator 的路径也有类似判断。CDI 自己的 populator util 会在 WFFC 且还没有 `volume.kubernetes.io/selected-node` annotation 时等待：[pkg/controller/populators/util.go#L125-L131](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/pkg/controller/populators/util.go#L125-L131)。vendor 里的 `lib-volume-populator` controller 也会等待 selected node：[vendor/github.com/kubernetes-csi/lib-volume-populator/populator-machinery/controller.go#L489-L493](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/vendor/github.com/kubernetes-csi/lib-volume-populator/populator-machinery/controller.go#L489-L493)。

## KubeVirt 如何接住这个状态

如果 CDI 只是停止 worker pod，事情还没完成。必须有另一个 controller 代表真正的 workload 去触发 PVC 绑定。KubeVirt 就是这个消费者。

KubeVirt VM controller 在检查 DataVolume 时，把 `Succeeded`、`WaitForFirstConsumer`、`PendingPopulation` 都视为可以继续往 VMI 侧推进的状态：[pkg/virt-controller/watch/vm/vm.go#L556-L558](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vm/vm.go#L556-L558)。注释里也明确写了：`WaitForFirstConsumer` 只能由 VMI 处理：[pkg/virt-controller/watch/vm/vm_test.go#L1245](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vm/vm_test.go#L1245)。

到了 VMI lifecycle，controller 会检查关联 DataVolume 是否 ready，同时得到 `isWaitForFirstConsumer` 标记：[pkg/virt-controller/watch/vmi/lifecycle.go#L91-L113](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vmi/lifecycle.go#L91-L113)。如果是 WFFC，它不是直接创建完整 virt-launcher Pod，而是调用 `RenderLaunchManifestNoVm(vmi)` 渲染一个不真正启动 VM 的临时 Pod：

```go
if isWaitForFirstConsumer {
    log.Log.V(3).Object(vmi).Infof("Scheduling temporary pod for WaitForFirstConsumer DV")
    templatePod, err = c.templateService.RenderLaunchManifestNoVm(vmi)
} else {
    templatePod, err = c.templateService.RenderLaunchManifest(vmi)
}
```

源码位置：[pkg/virt-controller/watch/vmi/lifecycle.go#L150-L156](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vmi/lifecycle.go#L150-L156)。等不再处于 WFFC 后，KubeVirt 会清理这些临时 Pod：[pkg/virt-controller/watch/vmi/lifecycle.go#L187-L190](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vmi/lifecycle.go#L187-L190)。

KubeVirt 对 PVC readiness 的判断也会调用 CDI API 的辅助函数 `IsWaitForFirstConsumerBeforePopulating()`，当 PVC 属于 DataVolume 且 DV phase 是 `WaitForFirstConsumer` 时返回 true：[CDI API utils.go#L45-L63](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/utils/utils.go#L45-L63)，KubeVirt 调用位置在 [pkg/storage/types/pvc.go#L232-L245](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/storage/types/pvc.go#L232-L245)。

对传统 import/upload/clone worker pod 路径，这个协作链路可以简化成：

```mermaid
flowchart TD
    DV["DataVolume 创建 PVC"]
    WFFC["StorageClass 是 WaitForFirstConsumer"]
    Pause["CDI 不创建 worker pod\nDV phase = WaitForFirstConsumer"]
    KV["KubeVirt 发现 DV 处于 WaitForFirstConsumer"]
    TempPod["KubeVirt 创建带 VMI 调度约束的临时 Pod"]
    Bind["scheduler 选择节点\nPVC/PV 确定拓扑"]
    Bound["PVC Bound"]
    Worker["CDI worker pod 导入、上传或克隆数据"]
    Done["DataVolume Succeeded"]
    Launcher["KubeVirt 创建真正的 virt-launcher Pod"]

    DV --> WFFC --> Pause --> KV --> TempPod --> Bind --> Bound --> Worker --> Done --> Launcher
```

如果走 CSI volume populator，则状态可能表现为 `PendingPopulation`，KubeVirt 也会把它当成可以继续推进到 VMI 侧的状态；但本质仍然是让使用方先提供调度上下文，再由存储/populator 流程继续填充数据。

KubeVirt API 里也有对应的 VMI condition：当 VMI 依赖处于 `Pending/WaitForFirstConsumer` 的 DataVolume，并且正在采取动作 provision PVC 时，会进入 `Provisioning` condition：[staging/src/kubevirt.io/api/core/v1/types.go#L655-L659](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/staging/src/kubevirt.io/api/core/v1/types.go#L655-L659)。

## 如何让它按 VM 的 selector 执行

如果目标是「让 CDI 根据 VM 上的 selector 选择节点」，配置入口不在 CDI worker pod 上，而在 VM/VMI 的调度字段上。正确做法是：

1. StorageClass 使用 `volumeBindingMode: WaitForFirstConsumer`。
2. CDI 开启 `HonorWaitForFirstConsumer`。
3. DataVolume 不加 `cdi.kubevirt.io/storage.bind.immediate.requested`，也不要使用 `virtctl image-upload --force-bind`。
4. 在 VM/VMI 上配置 `nodeSelector`、`affinity`、`tolerations`、`schedulerName`、`topologySpreadConstraints` 等调度约束。
5. 让 DataVolume 通过 `dataVolumeTemplates` 或 VM volume 引用进入 KubeVirt 的 VMI 创建流程。

KubeVirt 的临时 Pod 是从 VMI 渲染出来的。`RenderLaunchManifestNoVm()` 是 WFFC 时使用的临时 Pod 渲染入口：[pkg/virt-controller/services/template.go#L278-L285](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/services/template.go#L278-L285)。PodSpec 里会写入 VMI 派生的 `NodeSelector`、`SchedulerName`、`Tolerations`、`TopologySpreadConstraints` 等字段：[pkg/virt-controller/services/template.go#L686-L704](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/services/template.go#L686-L704)。其中 nodeSelector renderer 会先放入 KubeVirt 节点可调度标签，再合并集群级 selector 和 VMI 自身 selector：[pkg/virt-controller/services/nodeselectorrenderer.go#L33-L51](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/services/nodeselectorrenderer.go#L33-L51)。

下面用 `rancher/local-path-provisioner` 做一个最小例子。local-path provisioner 会基于配置在节点本地路径上动态创建 `hostPath` 或 `local` 类型 PV；它的 StorageClass 可以使用 `provisioner: rancher.io/local-path` 和 `volumeBindingMode: WaitForFirstConsumer`：[rancher/local-path-provisioner README](https://github.com/rancher/local-path-provisioner#storage-classes)。

```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path-wffc
provisioner: rancher.io/local-path
parameters:
  nodePath: /data/vm-disks
  pathPattern: "{{ .PVC.Namespace }}/{{ .PVC.Name }}/"
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-with-wffc-disk
spec:
  running: true
  dataVolumeTemplates:
    - metadata:
        name: vm-with-wffc-disk-root
      spec:
        source:
          http:
            url: "https://example.com/images/root.qcow2"
        pvc:
          storageClassName: local-path-wffc
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 20Gi
  template:
    spec:
      nodeSelector:
        node-role.kubernetes.io/virt-local: "true"
      tolerations:
        - key: "virt-local"
          operator: "Equal"
          value: "true"
          effect: "NoSchedule"
      domain:
        resources:
          requests:
            memory: 2Gi
        devices:
          disks:
            - name: root
              disk:
                bus: virtio
      volumes:
        - name: root
          dataVolume:
            name: vm-with-wffc-disk-root
```

这个例子里，CDI 会先创建 PVC，但不会让 import worker pod 抢先绑定。KubeVirt 看到 DV 处于 `WaitForFirstConsumer` 后，会创建带有 `nodeSelector: node-role.kubernetes.io/virt-local=true` 和对应 toleration 的临时 Pod。Kubernetes scheduler 根据这个临时 Pod 选择节点，local-path 在这个节点上创建本地 PV，PVC 绑定到符合 VM 调度约束的节点。PVC 绑定后，CDI 再继续导入数据。

```mermaid
flowchart TD
    VM["VM/VMI 带 nodeSelector"]
    DV["DataVolume 使用 local-path-wffc"]
    CDI["CDI honor WFFC\n不抢先创建 worker pod"]
    Temp["KubeVirt 创建临时 Pod"]
    Scheduler["scheduler 根据 VM selector 选节点"]
    LP["local-path 在该节点创建本地 PV"]
    Import["PVC Bound 后 CDI 导入数据"]
    Run["数据完成后 VM 启动"]

    VM --> DV --> CDI --> Temp --> Scheduler --> LP --> Import --> Run
```

这个方案能工作，但 local-path 的限制也要接受：

1. **它不是共享存储。** PV 会带节点亲和性，只能在创建它的节点上使用。local-path 官方文档也说明，默认会用 `kubernetes.io/hostname` 写入 PV node affinity，保证卷只能在创建节点访问。
2. **VM 后续基本被这个节点绑定。** 如果节点宕机或你想把 VM 调度到别的节点，本地盘数据不会自动迁移。
3. **更适合测试、边缘节点、单节点绑定或明确接受本地盘语义的场景。** 如果你需要 VM 可迁移、高可用或多节点漂移，应该考虑 Ceph RBD、Longhorn、LVM/CSI、云盘 CSI 等更适合虚拟机工作负载的存储。
4. **不要开启 immediate binding。** local-path 如果改成 `Immediate`，PVC 可能在没有 VM 调度上下文时就被 provision；这会绕开本文讨论的 WFFC 价值。
5. **不要给 DataVolume/PVC 加 immediate binding annotation。** 如果你给 DataVolume/PVC 加了 `cdi.kubevirt.io/storage.bind.immediate.requested`，或者上传时用了 `--force-bind`，就等于告诉 CDI 不要 honor WFFC，PVC 可能会被 CDI worker pod 先绑定。
6. **DataVolume 要通过 VM/VMI 消费。** 如果直接创建 DataVolume 而不是通过 VM/VMI 消费它，KubeVirt 不会有机会用 VM 的 selector 创建临时 Pod；这时要么等待真正消费者出现，要么接受 immediate binding。

## 可以用来做什么

`HonorWaitForFirstConsumer` 不是一个提升导入速度的功能，也不是一个存储兼容性兜底。它主要解决「谁来触发第一次绑定」的问题。

适合的应用场景包括：

1. **KubeVirt VM 使用本地盘或 HostPath 存储**

   本地盘 PV 天然绑定节点。让 CDI worker pod 先绑定，等于让导入任务替 VM 选节点。开启 `HonorWaitForFirstConsumer` 后，KubeVirt 的临时 Pod 先参与调度，PVC 位置更接近 VM 的真实运行位置。

2. **有 node selector、affinity、taints/tolerations 的 VM**

   例如 VM 只能跑在带 GPU、特定 CPU 型号、特定安全域或特定标签的节点。如果 CDI worker pod 没有完全一致的约束，提前绑定会制造不可调度风险。WFFC 的价值就是让 PVC 绑定时看到 VM 级别的调度约束。

3. **多磁盘 VM**

   多个 DataVolume 如果分别被 worker pod 提前绑定到不同节点，最终 VMI 可能找不到一个同时满足所有卷位置的节点。让 KubeVirt 先触发绑定，可以把多个 PVC 的拓扑选择放进同一个 VMI 调度上下文里。

4. **拓扑受限块存储**

   一些云盘、CSI 块存储或 zone 级存储不是全局可挂载。`WaitForFirstConsumer` 本来就是为这类场景设计的；CDI honor 这个行为后，不会用导入任务破坏 Kubernetes scheduler 的延迟绑定语义。

5. **热插拔卷**

   KubeVirt hotplug 路径也会识别 WFFC 卷：当卷还没有被 CDI 填充时，会创建 dummy pod 触发 population：[pkg/virt-controller/watch/vmi/volume-hotplug.go#L171-L181](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virt-controller/watch/vmi/volume-hotplug.go#L171-L181)。

## 什么时候不该用

有些场景反而不需要 honor WFFC。

如果只是上传一个通用 golden image，目标 PVC 不需要绑定到特定 VM 节点，立即创建 CDI upload pod 反而更简单。CDI 文档也把这类场景列为 immediate binding 的用途：[doc/waitforfirstconsumer-storage-handling.md#L28-L34](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/doc/waitforfirstconsumer-storage-handling.md#L28-L34)。

KubeVirt 的 `virtctl image-upload` 也暴露了同样的语义：`--force-bind` 的说明是「忽略 WaitForFirstConsumer logic，强制绑定 PVC」：[pkg/virtctl/imageupload/imageupload.go#L134-L139](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virtctl/imageupload/imageupload.go#L134-L139)。如果 DataVolume 处于 `WaitForFirstConsumer` 或 `PendingPopulation`，且没有 `--force-bind`，上传会报错提示先让 PVC Bound 或使用 force-bind：[pkg/virtctl/imageupload/imageupload.go#L539-L540](https://github.com/kubevirt/kubevirt/blob/357a5c9d62d5dc354f8927137144c9d76f6dcd6c/pkg/virtctl/imageupload/imageupload.go#L539-L540)。

所以可以按这个规则判断：

1. 这个 DataVolume 最终要给某个有明确调度约束的 VM 使用，应该 honor WFFC。
2. 这个 DataVolume 只是作为集群镜像、模板、通用数据源，不关心节点位置，可以 force bind。
3. 如果存储本身全局可访问，WFFC 的收益不明显，但通常也不会造成错误；主要代价是流程上要等第一个消费者出现。

## 结论

`HonorWaitForFirstConsumer` 的核心作用是保护 Kubernetes `WaitForFirstConsumer` 的延迟绑定语义，避免 CDI 的 import/upload/clone worker pod 抢先成为 PVC 的第一个消费者。

在 KubeVirt 场景里，它把职责拆开了：

1. CDI 负责识别 WFFC PVC，暂停 worker pod，并把 DataVolume 标记成 `WaitForFirstConsumer`。
2. KubeVirt 负责识别这个状态，用带有 VMI 调度约束的临时 Pod 触发 PVC 绑定。
3. PVC Bound 后，CDI 再继续数据导入、上传或克隆。
4. 数据完成后，KubeVirt 再启动真正的 VM Pod。

这个特性最适合节点或拓扑敏感的虚拟机磁盘场景，尤其是 local PV、HostPath、zone 级块存储、多磁盘 VM 和带强调度约束的 VMI。它解决的不是「如何导入数据」，而是「让谁先决定磁盘应该落在哪个节点或拓扑域」。

参考链接：

- Kubernetes StorageClass `volumeBindingMode` 官方文档：[Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/#volume-binding-mode)
- CDI WFFC 设计说明：[waitforfirstconsumer-storage-handling.md](https://github.com/kubevirt/containerized-data-importer/blob/420f86a0a68a96188338f33b043c0078a1704d3f/doc/waitforfirstconsumer-storage-handling.md)
- CDI 源码版本：[`420f86a0a68a96188338f33b043c0078a1704d3f`](https://github.com/kubevirt/containerized-data-importer/tree/420f86a0a68a96188338f33b043c0078a1704d3f)
- KubeVirt 源码版本：[`357a5c9d62d5dc354f8927137144c9d76f6dcd6c`](https://github.com/kubevirt/kubevirt/tree/357a5c9d62d5dc354f8927137144c9d76f6dcd6c)

