match 看不到的,preconditions 和 context 能看到
一句话总结
match/exclude 只按种类、名称、标签这类外在特征来选择。要想查看资源 spec 的内部,或者与其他资源比较之后再决定是否应用规则,就需要 preconditions 和 context。本文按照官方文档的 preconditions 一节和外部数据源一节,说明这两者以什么顺序评估,以及最多能做到什么程度。
为什么需要它
假设要写一条策略:“NodePort 类型的 Service 必须使用 externalTrafficPolicy: Local”。match 块只能选到 kinds: [Service],看不到 spec.type 是不是 NodePort。所以每个 Service 都会先命中这条规则,还需要第二道关卡,把其中的 NodePort 筛出来。这道关卡就是 preconditions。
第二种需求更常见。“这个命名空间里已经有几个 Pod”“允许的注册表列表在 ConfigMap 里,想和它比较”“想用 SubjectAccessReview 询问提出请求的人实际上有没有权限”。这类判断仅凭请求正文(AdmissionReview)无法做出,必须从别处获取数据。这就是 context。
工作原理
评估顺序
文档把顺序写得很清楚:资源命中 match 且没有被 exclude 排除之后,才评估 preconditions,整体为 TRUE 时才执行规则正文(validate、mutate 等)。match/exclude 中不能使用变量——变量文档给出的理由是“为了不读取数据就能快速选出规则”。而 preconditions 可以使用所有变量、JMESPath 和运算符。
在结果报告中两者也不同。命中 exclude 的资源会被直接忽略,而命中 match 却在 preconditions 中被淘汰的资源,会被记为 skip。如果在 PolicyReport 中看到很多 skip,那就是 preconditions 筛掉的。
any 与 all
preconditions 的表达式放在 any 或 all 块下面。any 是逻辑 OR,all 是逻辑 AND,同一条规则里也可以同时放两者。按文档的说法,“每个 any/all 块整体上必须为 TRUE”,规则才会继续,只要有一个不是 TRUE,规则就不会被应用。它与 deny 规则的 conditions 结构相同,并以同样的方式进行短路求值(short circuiting)。
preconditions:
any:
- key: "{{ request.object.metadata.labels.color || '' }}"
operator: Equals
value: blue
- key: "{{ request.object.metadata.labels.app || '' }}"
operator: Equals
value: busybox
all:
- key: "{{ request.object.metadata.labels.env || '' }}"
operator: Equals
value: qa
单个表达式由 key、operator、value 构成。运算符有 Equals、NotEquals,GreaterThan、GreaterThanOrEquals、LessThan、LessThanOrEquals,集合比较的 AnyIn、AllIn、AnyNotIn、AllNotIn,以及时长比较的 DurationGreaterThan 系列。上面示例中的 || '' 是一种惯用写法:在标签不存在时用空字符串接住,避免 JMESPath 评估失败,处理可选字段时几乎总是需要它。
来自请求的变量
Kyverno 预先提供的变量来自 AdmissionReview。request.object 是被创建或被修改的对象(在 DELETE 中为 null),request.oldObject 是修改之前的对象(在 CREATE 中为 null),request.operation 是 CREATE、UPDATE、DELETE、CONNECT 之一,request.userInfo 包含 username 和 groups,request.namespace 是目标命名空间。此外还有 serviceAccountName、serviceAccountNamespace,request.roles、request.clusterRoles,以及保存容器镜像信息的 images。
有一个陷阱。如果在规则中使用像用户名这样的只存在于准入请求中的变量,就必须把策略的 background 设为 false。因为后台扫描是重新扫描已有的资源,没有请求信息,CRD 文档中 kubectl explain 的输出里就原样写着这个条件。
context——从别处获取数据
context 条目定义在规则中,并按定义的顺序评估。前面的变量可以在后面引用,但在前面引用后面的就是错误。共有五种。
| 类型 | 作用 | 引用方式 |
|---|---|---|
configMap |
通过 name、namespace 读取 ConfigMap |
{{ 이름.data.키 }}(占位符依次为名称、键) |
apiCall |
调用 Kubernetes API 或外部服务 | urlPath + jmesPath |
globalReference |
引用预先缓存的 GlobalContextEntry | name |
imageRegistry |
获取 OCI 镜像的元数据 | reference + jmesPath |
variable |
保存用 JMESPath 计算出的值 | jmesPath + default |
apiCall 使用与 kubectl get --raw 完全相同的路径。所以文档建议在放进策略之前,先像下面这样手动试一下。
kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp query "items | length(@)"
把同样的内容搬进策略,就是下面这样。urlPath 中也可以使用变量,默认方法是 GET,给出 method: POST 和 data 后,也可以调用 SubjectAccessReview 这类写入型 API。为了防备 API 服务器返回错误,可以用 default 设置替代值。
context:
- name: podCount
apiCall:
urlPath: "/api/v1/namespaces/{{ request.namespace }}/pods"
jmesPath: "items | length(@)"
default: 0
对于 configMap,只要给 ConfigMap 加上 cache.kyverno.io/enabled: "true" 标签,Kyverno 就会自动缓存它,每次策略判断时就不用去调用 API 服务器。globalReference 则更进一步:在 GlobalContextEntry 资源中声明 kubernetesResource(group、version、resource、namespace)或 apiCall(外部调用 + refreshInterval),Kyverno 就会用 informer 维护缓存,多条策略共用同一份缓存。不过,如果 GlobalContextEntry 没有就绪,引用它的策略也会处于未就绪状态而不被处理。
权限也不能忘。要用 apiCall 读取某种资源,Kyverno 控制器的 ClusterRole 就必须具有该权限,安装自定义文档介绍了创建带有 rbac.kyverno.io/aggregate-to-admission-controller: "true" 这类标签的 ClusterRole 并进行聚合(aggregate)的方式。从 1.13 起通配符 view 权限已被去掉,所以自定义资源必须显式开放。
在现场相遇的样子
某个团队加入了一条规则:“每个命名空间最多两个 LoadBalancer 类型的 Service”。用 match 数不出个数,所以他们用 apiCall 取得该命名空间的 Service 列表,用 items[?spec.type == 'LoadBalancer'] | length(@) 计数,再用 preconditions 的 GreaterThanOrEquals,只在数量大于等于 2 时才 deny。起初准入延迟明显增加,原因是每个请求都会发出一次 API 调用。正如文档所说,API 调用会在每个准入请求中执行,所以答案是把常用数据迁移到 GlobalContextEntry 中缓存。
另一个团队遇到的情况是,把用户名放进 preconditions 的策略,在 PolicyReport 中显得很奇怪。后台扫描中没有 request.userInfo,所以那条策略必须是 background: false。
接下来的文章要讲什么
下一篇文章会讲 Pod 规则被自动生成为 Deployment、CronJob 规则的 autogen,以及策略删除资源的 cleanup。接下来的测验会确认本文的评估顺序、skip 与 exclude 的区别,以及 context 的五种类型。