OPA 策略实战:用 `units.parse` 统一解析 Mi/Gi 等内存单位,实现跨单位资源限额比较
2026/9/23 14:14:31 网站建设 项目流程

OPA 策略实战:用units.parse统一解析 Mi/Gi 等内存单位,实现跨单位资源限额比较

【免费下载链接】opaOpen Policy Agent (OPA) is an open source, general-purpose policy engine.项目地址: https://gitcode.com/gh_mirrors/op/opa

容器规格中的内存限制往往以多种后缀形式出现(MiGi乃至裸数字),而 Kubernetes 命名空间级配额又是另一个单位体系,直接做字符串比较毫无意义。本文以 Open Policy Agent (OPA) 仓库内完整的可运行示例 memory-limits 为主线,讲解 Rego 内置函数units.parse的语法、单位表与大小写规则,并深入其 Go 实现,让你能够在一条策略中把不同单位的资源值统一为数字后安全比较。

读完本文,你将掌握:units.parse的完整单位清单与错误行为、一个"容器内存超限即拒绝"的端到端 Rego 策略、以及该内置函数在 v1/topdown/parse_units.go 中的底层解析原理。

问题背景:为什么需要units.parse

在 Kubernetes 等容器编排环境中,资源规格的书写习惯并不统一:

  • 有的字段使用二进制后缀:512Mi1Gi
  • 有的使用十进制后缀:2G500M
  • 还有的直接给裸数字:1073741824

而命名空间配额(namespace cap)通常又是一个单独配置的值,例如1Gi。如果策略代码里直接写input.container.memory_limit > input.namespace_memory_limit,比较的将是字符串"2Gi""1Gi",按字典序的结果不可靠;若手工写"2Gi" == "2048Mi"之类的等价表,又会随着单位组合爆炸而难以维护。

OPA 提供的units.parse内置函数正是为解决这个问题而生:它把带可选单位后缀的字符串转换成数字,让策略可以用纯数值进行比较。官方文档 Unit Built-ins 明确说明,其常见用途就是"在同一条策略检查中比较带不同后缀到达的资源限额"。

units.parse语法与单位表

units.parse(string) => number接收一个字符串参数,返回一个数字。它支持三类写法:

类别后缀说明
十进制 SI 后缀KMGTPE(以及小写kgtpe按 1000 进位:1K = 1000
二进制后缀KiMiGiTiPiEi(以及kimi等小写首字母变体)按 1024 进位:1Ki = 1024
毫(milli)小写m1m = 0.001

关键的大小写规则来自 units.mdx 与源码注释:

  • mM严格区分大小写:小写m表示毫(milli,0.001),大写M表示兆(mega,1000000);
  • 其余后缀如K/kG/g大小写均可接受(首字母大小写不敏感);
  • 裸数字(无后缀)同样合法,直接原样解析为数值。

从 parse_units.go 的常量定义可以看到十进制进位的实现:siK = 1000siM = siK * 1000siG = siM * 1000,依次递推到siE;二进制常量(kimigi等)则按源码注释说明借用自topdown/parse_bytes,以 1024 为底。

完整示例:按命名空间配额拒绝超限容器

仓库中的示例目录 memory-limits 包含策略、输入、输出与 playground 配置五个文件,下面逐一展开。

策略文件 policy.rego

package play # Cap for the whole namespace, normalized to a number. max_memory := units.parse(input.namespace_memory_limit) # Containers whose limit is above the namespace cap. deny contains msg if { some c in input.containers units.parse(c.memory_limit) > max_memory msg := sprintf( "container %q memory limit %s exceeds namespace cap %s", [c.name, c.memory_limit, input.namespace_memory_limit], ) }

策略共两个部分:

  1. max_memory:把命名空间的配额字符串"1Gi"通过units.parse归一化为数字1073741824,作为全命名空间统一的上限基准;
  2. deny规则:遍历input.containers中的每个容器,把各自的memory_limit(如"512Mi""2Gi")也解析为数字,与max_memory做数值比较;一旦超限,就用sprintf生成一条包含容器名、原字符串限制值和命名空间配额的可读消息。

注意这里刻意保留了c.memory_limitinput.namespace_memory_limit原始字符串用于生成报错消息——解析后的数字只用于比较,展示给用户时仍使用人类友好的原始表述。

输入 input.json

{ "namespace_memory_limit": "1Gi", "containers": [ { "name": "app", "memory_limit": "512Mi" }, { "name": "batch", "memory_limit": "2Gi" } ] }

输入模拟了一次真实的准入审查场景:命名空间配额为1Gi,有两个容器——app申请512Mi(低于配额),batch申请2Gi(明显超限)。

输出 output.json

{ "deny": [ "container \"batch\" memory limit 2Gi exceeds namespace cap 1Gi" ], "max_memory": 1073741824 }

结果验证了策略逻辑:

  • max_memory被正确解析为1073741824,即1Gi = 2^30(二进制进位的直接体现);
  • deny只包含batch容器的拒绝消息,app512Mi换算为536870912,未超过配额,因此不产生拒绝记录。

该示例在 playground 中的展示配置 config.json(showInput: trueshowData: false)表明示例默认展示输入而不展示 data,便于读者直接对照输入与输出理解单位换算关系。

源码级原理:units.parse的 Go 实现

units.parse的实现在 v1/topdown/parse_units.go,注册入口为:

func init() { RegisterBuiltinFunc(ast.UnitsParse.Name, builtinUnits) }

整个builtinUnits函数的处理流程可以拆成四步:

  1. 取字符串并清理:通过builtins.StringOperand取出第一个操作数,并移除字符串中的转义引号(strings.ReplaceAll(s, "\"", "")),与units.parse_bytes保持行为一致;
  2. 空格校验:只要字符串中包含空格就直接返回errIncludesSpaces("spaces not allowed in resource strings"),因此形如"1 Gi"的写法是非法输入;
  3. 拆分数字与单位:调用extractNumAndUnit拆出数值部分与后缀;数值为空时报errNoAmount,指数过大时报errUnitsExponentTooLarge
  4. 单位换算与精度处理:核心细节在大小写归一化上——只把第一个字母之后的部分转为小写unit[:1] + strings.ToLower(unit[1:])),这样Mm得以保留区别:"m"命中毫分支(siMilli = 0.001),"M"命中十进制兆分支;"Mi"命中二进制兆分支。未知后缀则返回errUnitNotRecognized

数值计算采用math/bigbig.Rat(有理数)而非浮点数。源码注释给出了明确理由:像0.001这样的量在二进制浮点中无法精确表示,big.Rat不会出现浮点精度问题。最终输出时,若结果为纯整数(如1073741824)直接输出整数;否则用FloatString(10)保留 10 位小数输出,例如1m会得到0.001

常见的报错信息一览

触发条件错误信息
字符串含空格spaces not allowed in resource strings
无数值部分(如只传Gino amount provided
数值无法解析为数字could not parse amount to a number
后缀无法识别unit <xxx> not recognized
指数过大exponent too large

这些错误行为都体现在 parse_units.go 的预定义错误变量中,可供你在调试策略时对照排查。

实战建议与扩展思考

1. 统一在数据侧做归一化

像示例中的max_memory := units.parse(input.namespace_memory_limit)这样,先在策略开头把所有参照值归一化为数字,后续所有规则都基于纯数值比较,可显著降低维护成本。若归一化逻辑需要复用,也可以把它封装为自定义规则或函数,供多个deny规则共享。

2. 保留原始字符串用于报错

units.parse只负责"换算",不负责"展示"。示例中把原始"2Gi""1Gi"拼进sprintf消息的做法值得推广:比较用数字,报告用原文,既准确又易读。

3. 警惕大小写陷阱

这是最常见的踩坑点:500M是五亿(十进制兆),500m是 0.5(毫),500Mi是约 5.24 亿(二进制兆)。在写策略或生成输入数据时务必确认字段的单位约定,否则一次M/m之差就会导致配额放宽 10 亿倍。

4. 与units.parse_bytes的关系

units.parse_bytes是面向字节场景的兄弟函数,二者共享单位常量设计(二进制常量即借用自parse_bytes)。区别在于units.parse额外支持毫(m)且对大小写更敏感,适用于内存、CPU 等更细粒度的资源换算场景。

参考资源

  • 示例入口:memory-limits/intro.md
  • 完整策略:policy.rego
  • 示例输入 / 期望输出:input.json / output.json
  • 内置函数文档:builtins/units.mdx
  • Go 实现:v1/topdown/parse_units.go

至此,你已经能够像示例一样,用一行units.parse把千差万别的内存单位统一成可比较的数字,让容器限额审查策略既严谨又易读。

【免费下载链接】opaOpen Policy Agent (OPA) is an open source, general-purpose policy engine.项目地址: https://gitcode.com/gh_mirrors/op/opa

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询