
有些调度、监控或运维组件只能读取 Pod Label，但 KubeVirt 中业务侧维护的是 `VirtualMachine`。如果 VM 已经运行，只修改 `spec.template.metadata.labels`，怎样让 Label 继续更新到当前的 `VirtualMachineInstance` 和 `virt-launcher` Pod？

KubeVirt 当前 `main` 提供了 `virt-controller` 参数 `--additional-launcher-labels-sync`。它可以按完整 key 或前缀选择 Label，并沿着下面的方向单向同步：

```text
VM.spec.template.metadata.labels -> VMI.metadata.labels -> virt-launcher Pod.metadata.labels
```

我在独立的 Kubernetes 集群中用两台 Label 不同的 VM 做了在线修改和删除测试。结果是 VMI 与现有 Pod 都能更新，并且 Pod UID 不变，不需要重建 VM Pod。

<!--more-->

> 本文验证的是 KubeVirt `main` 的 commit [`3e83f1130a`](https://github.com/kubevirt/kubevirt/commit/3e83f1130ac74e43061b5a4cb04d81efa4e29bdb)。不要据此判断某个旧版本已经包含这项能力，使用前应检查目标版本源码或 `virt-controller --help`。

## 配置要同步的 Label

通过 KubeVirt CR 给 `virt-controller` 增加参数：

```yaml
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  customizeComponents:
    flags:
      controller:
        additional-launcher-labels-sync: "ops.example.com/*"
```

也可以直接 patch 现有 KubeVirt CR：

```bash
kubectl -n kubevirt patch kubevirt kubevirt --type=merge -p '{
  "spec": {
    "customizeComponents": {
      "flags": {
        "controller": {
          "additional-launcher-labels-sync": "ops.example.com/*"
        }
      }
    }
  }
}'
```

如果只允许固定的几个 key，可以使用逗号分隔：

```yaml
additional-launcher-labels-sync: "ops.example.com/owner,ops.example.com/vm-id"
```

如果每台 VM 的 key 也不同，使用末尾带 `*` 的前缀即可：

```yaml
additional-launcher-labels-sync: "ops.example.com/*"
```

例如两台 VM 可以分别使用：

```yaml
# VM A
labels:
  ops.example.com/vm-id: vm-a
  ops.example.com/owner-a: team-a

# VM B
labels:
  ops.example.com/vm-id: vm-b
  ops.example.com/owner-b: team-b
```

它们不需要拥有相同的 Label key。控制器匹配的是前缀，而不是预先枚举每台 VM 的完整 Label 集合。Kubernetes Label 的 prefix/name 格式可以参考 [Labels and Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)。

源码中的参数说明同时给出了精确语义：支持完整 key、支持末尾 `*` 的前缀，并且同步方向是 VM template 到 VMI，再到 launcher Pod。[^flag]

单独配置 `"*"` 不会同步全部 Label。两段同步实现都会拒绝空前缀，避免一次配置错误把所有 Label 复制到 Pod。[^vm-sync][^pod-sync]

## 修改 VM 的 template，而不是 VM metadata

需要修改的是：

```text
spec.template.metadata.labels
```

不是：

```text
metadata.labels
```

下面的命令会在线修改 VM A 的 owner，并增加一个新 Label：

```bash
kubectl -n vm-demo patch vm vm-a --type=merge -p '{
  "spec": {
    "template": {
      "metadata": {
        "labels": {
          "ops.example.com/owner-a": "team-a-v2",
          "ops.example.com/dynamic-a": "enabled"
        }
      }
    }
  }
}'
```

可以同时查看 VM、VMI 和 Pod：

```bash
kubectl -n vm-demo get vm vm-a \
  -o jsonpath='{.spec.template.metadata.labels}'

kubectl -n vm-demo get vmi vm-a \
  -o jsonpath='{.metadata.labels}'

kubectl -n vm-demo get pod -l vm.kubevirt.io/name=vm-a \
  -o jsonpath='{.items[0].metadata.labels}'
```

确认参数已经进入 `virt-controller` 时，需要一起检查 container 的 `command` 和 `args`：

```bash
kubectl -n kubevirt get deploy virt-controller \
  -o jsonpath='{.spec.template.spec.containers[0].command}{" "}{.spec.template.spec.containers[0].args}{"\n"}'
```

## 控制器如何完成同步

这不是 VM 重启流程，而是两段独立的 reconcile：

```mermaid
flowchart LR
    VM["VirtualMachine<br/>spec.template.labels"]
    VC1["VM Controller<br/>筛选 exact key 或 prefix"]
    VMI["VirtualMachineInstance<br/>metadata.labels"]
    VC2["VMI Controller<br/>筛选同一组 key"]
    Pod["现有 virt-launcher Pod<br/>metadata.labels"]

    VM --> VC1 -->|"Patch VMI"| VMI
    VMI --> VC2 -->|"Patch Pod"| Pod
```

第一段位于 VM Controller 的 `syncDynamicAnnotationsAndLabelsToVMI`。它会克隆 VMI 当前的 Label，再按配置逐个处理：[^vm-sync]

1. 完整 key 直接比较 VM template 与 VMI 中的值。
2. 前缀模式会遍历 VM 和 VMI 中匹配前缀的 key。
3. VM 中存在的 key 会新增或覆盖到 VMI。
4. VMI 中存在、但 VM template 已删除的 key 会从 VMI 删除。
5. 有变化时使用带 `test` 的 JSON Patch 更新 VMI。

第二段位于 VMI Controller 的 `syncDynamicAnnotationsAndLabelsToPod`，使用相同规则把 VMI Label patch 到现有 launcher Pod。[^pod-sync]

这也解释了为什么删除能够继续传播：前缀匹配不只遍历源对象，也遍历目标对象。目标中多出来的匹配 key 会被识别并删除。

## 隔离集群中的验证

测试环境如下：

| 项目 | 值 |
| --- | --- |
| Kubernetes | `v1.36.3` |
| KubeVirt | `main@3e83f1130a` |
| Provider | `kubevirtci k8s-1.36` |
| 同步前缀 | `codex.test/*` |

我使用 `make cluster-up` 创建独立集群，再通过 `make cluster-sync` 部署当前源码。没有使用或修改已有 Kubernetes 集群。

测试创建了两台 VM：

| VM | 初始 Label | 在线更新 |
| --- | --- | --- |
| A | `owner-a=team-a-v1` | `owner-a=team-a-v2`，新增 `dynamic-a=enabled` |
| B | `owner-b=team-b-v1` | `owner-b=team-b-v2`，新增 `dynamic-b=blue` |

VM A 更新后，四个观测值一致：

```text
VMI owner-a     = team-a-v2
VMI dynamic-a   = enabled
Pod owner-a     = team-a-v2
Pod dynamic-a   = enabled
```

更新前后的 Pod UID 都是：

```text
3148fea0-fa94-4311-863a-fdb959ef15d2
```

随后从 VM template 删除 `dynamic-a`，VMI 和 Pod 上的 `dynamic-a` 都被删除，Pod UID 仍未变化。

VM B 使用完全不同的 `owner-b` 和 `dynamic-b`。更新后的 VM template、VMI 和 Pod 都是：

```text
owner-b  = team-b-v2
dynamic-b = blue
```

Pod 上没有出现 VM A 的 `owner-a`，更新前后的 Pod UID 都是：

```text
b721533b-c5f7-4484-a987-bb50f62e9876
```

隔离集群只有一个节点，同时运行两台 VMI 时内存不足，因此测试按 A、B 顺序执行。这个限制影响并发运行数量，不影响每条同步链路的验证。

测试结束后删除了测试 Namespace，并执行 `KUBEVIRT_PROVIDER=k8s-1.36 make cluster-down`。节点容器和测试网络均已删除。

## 使用时需要注意的边界

1. **同步是单向的。** 修改 VMI 或 Pod 不会回写 VM template，后续 reconcile 还可能被 template 覆盖。
2. **只同步配置命中的 key。** 其他 VM/VMI/Pod Label 不会因为这个参数被统一复制。
3. **不要使用裸 `*`。** 当前实现会忽略它；应使用自己的域名前缀，例如 `ops.example.com/*`。
4. **Label 更新可能改变其他控制器行为。** 如果 Service、NetworkPolicy、监控或调度逻辑依赖这些 Pod Label，应先在测试环境验证 selector 的影响。
5. **关闭参数不等于清理旧 Label。** 从实现看，控制器只遍历当前配置的 key 或前缀。移除配置后，它不再知道哪些旧 Label 应删除；需要先从 VM template 删除并等待同步完成，再关闭参数。
6. **先确认版本。** 本文只证明 commit `3e83f1130a` 上的实际行为，没有验证某个正式 release 的首次支持版本。

对于「每台 VM 的 Label 都不同」这个场景，关键不是把所有 key 写进 controller 参数，而是先规划一个专用前缀，再让每台 VM 在这个前缀下自由定义自己的 key。这样既保留了 VM 之间的差异，也把允许传播到 launcher Pod 的范围限制在可审查的边界内。

[^flag]: KubeVirt [`application.go`](https://github.com/kubevirt/kubevirt/blob/3e83f1130ac74e43061b5a4cb04d81efa4e29bdb/pkg/virt-controller/watch/application.go#L1124-L1129) 中的 `additional-launcher-labels-sync` 参数定义。
[^vm-sync]: KubeVirt VM Controller 的 [`syncDynamicAnnotationsAndLabelsToVMI`](https://github.com/kubevirt/kubevirt/blob/3e83f1130ac74e43061b5a4cb04d81efa4e29bdb/pkg/virt-controller/watch/vm/vm.go#L3067-L3169)。
[^pod-sync]: KubeVirt VMI Controller 的 [`syncDynamicAnnotationsAndLabelsToPod`](https://github.com/kubevirt/kubevirt/blob/3e83f1130ac74e43061b5a4cb04d81efa4e29bdb/pkg/virt-controller/watch/vmi/lifecycle.go#L724-L826)。

