最近、Conftestを勉強し始めたところ、デバッグ用のtraceの挙動で悩んだので使い方と注意点を記載する。
Conftestを簡単に説明すると、ConftestとはYAMLやJSONなどの構造化データに対するテストを作成するツールである。
例えばk8sの設定ファイルのテストなどに使われていて、テストはOpenPolicyAgent(OPA)のRego言語を用いて記述する。
(Regoは"ray-go"と発音するので、日本語では"レイゴー")
trace の使い方
ConftestのRego言語ではデバッグのためにtraceをコード内に埋め込み、表示させることができる。
OpenPolicyAgent - Policy Reference - Built-in Functions - Debugging
環境
conftest v0.24.0
opa v0.30.2
コード
traceで文字列を表示させる場合は引数として文字列を渡す。
trace("Hello")
変数を埋め込みたい場合はsprintf
を使う。
trace(sprintf("foo: %v", [input.foo]))
sprintfの第二引数には変数を指定できるが、変数には[]が必要である。
(変数に[]をつけない場合の動作は後述。)
実行方法
traceを表示するにはconftest test
やconftest verify
に--trace
を追加して実行する。
traceの注意点
おさらい
traceの注意点の前にRego言語の構文と名前についておさらいする。
- Regoの構文では、ルール(Rules)のbodyに式(expression)を置く。
式は結果がtrueになるように記載し、全ての式がtrueの場合はルールが成功する。 - 次のように式が2行ある場合は論理積(Logical AND)として処理されるので、
2つの式の結果がtrueであれば、ルールは成功となる。 - 逆に式の結果のどちらかがfalseであれば、ルールは失敗となるし、
式の結果がfalseだった時点でルールの処理が中断され、falseだった式以降は処理されない。
deny[msg] { #ルール(Rules)のhead
#ルール(Rules)のbody
input.foo == "one" #式(expression)
input.bar == "second" #式(expression)
msg := "Output trace message." #変数(variables)
}
注意点
OPAのドキュメントでは明確に書かれていないように見えるが、
traceは式(expression)の1つであるため、traceを表示するには次の注意点がある。
- traceより前の式(expression)の結果で、traceの出力が変わる。
例えば、traceより前の式の結果がtrue なら、式のあとのtraceは表示される。
一方で、traceより前の式の結果がfalseなら、式のあとのtraceは表示されない。
これはtraceより前の式の結果がfalseだった時点でルールの評価が中断され、ルールが失敗するため。 - trace内のsprintfの第二引数に存在しない変数を指定した場合や変数に[]をつけ忘れた場合、ルールが失敗する。
これはtraceの式としての結果がfalseになり、ルールの処理が中断され、ルールが失敗するため。
実際にコードを用いて注意点を確認する。
注意点の例
例1:traceより前の式の結果でtraceの出力有無が変わる
ファイルと実行コマンド
policy_trace_behavior/trace.rego
package main
deny_trace_message[msg] {
# 式の前にある trace は必ず出力する。
trace(sprintf("[Before expression] foo: %v", [input.foo]))
input.foo == "before & after"
# 式の結果が true なら 次のtrace は出力する。
# 式の結果が false なら 次のtrace は出力されない。
trace(sprintf("[After expression] foo: %v", [input.foo]))
msg := "Output trace message."
}
policy_trace_behavior/trace_test.rego
package main
test_trace_show_before_and_after {
msg := "Output trace message."
json := {
# 式(expression)はtrueになる
"foo": "before & after"
}
deny_trace_message[msg] with input as json
}
test_trace_show_only_before {
msg := "Output trace message."
json := {
# 式(expression)はfalseになる
"foo": "before"
}
deny_trace_message[msg] with input as json
}
conftest verify --policy policy_trace_behavior --trace
traceより前の式の結果がtrueの場合
traceより前の式の結果がtrueだった場合は、式の前後のtraceが表示された。(ハイライト部分)
file: policy_trace_behavior/trace_test.rego | query: test_trace_show_before_and_after
TRAC Enter data.main.test_trace_show_before_and_after = _
TRAC | Eval data.main.test_trace_show_before_and_after = _
TRAC | Index data.main.test_trace_show_before_and_after (matched 1 rule)
TRAC | Enter data.main.test_trace_show_before_and_after
TRAC | | Eval msg = "Output trace message."
TRAC | | Eval json = {"foo": "before & after"}
TRAC | | Eval data.main.deny_trace_message[msg] with input as json
TRAC | | Index data.main.deny_trace_message (matched 1 rule)
TRAC | | Enter data.main.deny_trace_message
TRAC | | | Eval __local7__ = input.foo
TRAC | | | Eval sprintf("[Before expression] foo: %v", [__local7__], __local5__)
TRAC | | | Eval trace(__local5__)
TRAC | | | Note "[Before expression] foo: before & after"
TRAC | | | Eval input.foo = "before & after"
TRAC | | | Eval __local8__ = input.foo
TRAC | | | Eval sprintf("[After expression] foo: %v", [__local8__], __local6__)
TRAC | | | Eval trace(__local6__)
TRAC | | | Note "[After expression] foo: before & after"
TRAC | | | Eval msg = "Output trace message."
TRAC | | | Exit data.main.deny_trace_message
TRAC | | Exit data.main.test_trace_show_before_and_after
TRAC | Exit data.main.test_trace_show_before_and_after = _
(snip)
traceより前の式の結果がfalseの場合
traceより前の式の結果がfalseだった場合は、式のあとのtraceは表示されない。
falseだった式より前のtraceのみが表示された。(ハイライト部分1つ目)
ルールもFailした(ハイライト部分2つ目)
file: policy_trace_behavior/trace_test.rego | query: test_trace_show_only_before
TRAC Enter data.main.test_trace_show_only_before = _
TRAC | Eval data.main.test_trace_show_only_before = _
TRAC | Index data.main.test_trace_show_only_before (matched 1 rule)
TRAC | Enter data.main.test_trace_show_only_before
TRAC | | Eval msg = "Output trace message."
TRAC | | Eval json = {"foo": "before"}
TRAC | | Eval data.main.deny_trace_message[msg] with input as json
TRAC | | Index data.main.deny_trace_message (matched 1 rule)
TRAC | | Enter data.main.deny_trace_message
TRAC | | | Eval __local7__ = input.foo
TRAC | | | Eval sprintf("[Before expression] foo: %v", [__local7__], __local5__)
TRAC | | | Eval trace(__local5__)
TRAC | | | Note "[Before expression] foo: before"
TRAC | | | Eval input.foo = "before & after"
TRAC | | | Fail input.foo = "before & after"
TRAC | | | Redo trace(__local5__)
TRAC | | | Redo sprintf("[Before expression] foo: %v", [__local7__], __local5__)
TRAC | | | Redo __local7__ = input.foo
TRAC | | Fail data.main.deny_trace_message[msg] with input as json
TRAC | | Redo json = {"foo": "before"}
TRAC | | Redo msg = "Output trace message."
TRAC | Fail data.main.test_trace_show_only_before = _
例2:trace内のsprintfで構文ミスした場合は、ルールがFailする。
ファイルと実行コマンド
policy_trace_expression/trace.rego
package main
# sprintfの第二引数には[]をつけない場合の例
# traceの式の結果がfalseになるので、ルールはFAIL になる。
deny_invalid_2nd_argument[msg] {
trace(sprintf("[Invalid 2nd argument] foo: %v", input.foo))
input.foo == "one"
msg := "Invalid 2nd argument"
}
# sprintfの第二引数が存在しない場合の例
# traceの式の結果がfalseになるので、ルールはFAIL になる。
deny_not_exist_2nd_argument[msg] {
trace(sprintf("[Invalid 2nd argument] foo: %v", [input.bar]))
input.foo == "one"
msg := "Not exist 2nd argument"
}
policy_trace_expression/trace_test.rego
package main
# FAIL
test_invalid_2nd_argument1 {
msg := "Invalid 2nd argument"
json := {
"foo": "one"
}
deny_invalid_2nd_argument[msg] with input as json
}
# FAIL
test_not_exist_2nd_argument1 {
msg := "Not exist 2nd argument"
json := {
"foo": "one"
}
deny_not_exist_2nd_argument[msg] with input as json
}
conftest verify --policy policy_trace_expression --trace
sprintfの第二引数には[]をつけない場合の例
sprintfでFailし、ルールもFailした。(ハイライト部分)
file: policy_trace_expression/trace_test.rego | query: test_invalid_2nd_argument1
TRAC Enter data.main.test_invalid_2nd_argument1 = _
TRAC | Eval data.main.test_invalid_2nd_argument1 = _
TRAC | Index data.main.test_invalid_2nd_argument1 (matched 1 rule)
TRAC | Enter data.main.test_invalid_2nd_argument1
TRAC | | Eval msg = "Invalid 2nd argument"
TRAC | | Eval json = {"foo": "one"}
TRAC | | Eval data.main.deny_invalid_2nd_argument[msg] with input as json
TRAC | | Index data.main.deny_invalid_2nd_argument (matched 1 rule)
TRAC | | Enter data.main.deny_invalid_2nd_argument
TRAC | | | Eval __local14__ = input.foo
TRAC | | | Eval sprintf("[Invalid 2nd argument] foo: %v", __local14__, __local12__)
TRAC | | | Fail sprintf("[Invalid 2nd argument] foo: %v", __local14__, __local12__)
TRAC | | | Redo __local14__ = input.foo
TRAC | | Fail data.main.deny_invalid_2nd_argument[msg] with input as json
TRAC | | Redo json = {"foo": "one"}
TRAC | | Redo msg = "Invalid 2nd argument"
TRAC | Fail data.main.test_invalid_2nd_argument1 = _
sprintfの第二引数が存在しない場合の例
jsonaの読み込みあたりでFailし、ルールもFailした。(ハイライト部分)
file: policy_trace_expression/trace_test.rego | query: test_not_exist_2nd_argument1
TRAC Enter data.main.test_not_exist_2nd_argument1 = _
TRAC | Eval data.main.test_not_exist_2nd_argument1 = _
TRAC | Index data.main.test_not_exist_2nd_argument1 (matched 1 rule)
TRAC | Enter data.main.test_not_exist_2nd_argument1
TRAC | | Eval msg = "Not exist 2nd argument"
TRAC | | Eval json = {"foo": "one"}
TRAC | | Eval data.main.deny_not_exist_2nd_argument[msg] with input as json
TRAC | | Index data.main.deny_not_exist_2nd_argument matched 0 rules)
TRAC | | Fail data.main.deny_not_exist_2nd_argument[msg] with input as json
TRAC | | Redo json = {"foo": "one"}
TRAC | | Redo msg = "Not exist 2nd argument"
TRAC | Fail data.main.test_not_exist_2nd_argument1 = _
最後に
ここまでの記載の通り、traceは式の1つとして動作するため、traceの成否がルールの成否にも関わってくる。
個人的にはtraceはルールの成否にかかわらずデバッグプリントとしてのみ動作して欲しいが、そのような動作ではないので、
traceの挙動を理解した上でのデバッグプリントが必要である。