1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

conftestで複数ファイルを横断してチェックする

記事一覧はこちら

背景・モチベーション

以前にconftestの概要をざっくり知って試してみたという記事を出しました。

44smkn.hatenadiary.com

実際に実務で利用してみると、1ファイル内だけのテストだけでなく、複数のファイルを横断的に見てチェックを行いたいという需要がありました。
例えば、Serviceで指定しているラベルがDeploymentでのpodのlabelに含まれるかといった検査です。

--combineオプションを利用する

contestのv0.24.0で試しています

--combineオプションとは

www.conftest.dev

上記のドキュメントから確認できるように、--combineというオプションをconftest実行時に付けてあげると複数ファイルを一度にロードしてくれます。しかしながら、単一のファイル向けに宣言していたRuleは正常に動かなくなるため、このオプションを利用する場合には書き直す必要がありそうです。
--combineと毎回宣言するのも手間なので、自分はconftestを実行するディレクトリに、conftest.tomlを配置して設定を行っています。

combine = true

--combineオプションを利用した時のinputの内容が変わる

このcombineオプションを利用すると、inputに入ってくるドキュメントの内容が変わってきます。inputの中身を確かめるだけのpolicyを書いて実行してみます。

# policy/combine.rego
package main

deny[msg] {
    msg = json.marshal(input)
}

入力となるマニフェストは下記です。

# manifests.yaml
kind: Service
metadata:
  name: hello-kubernetes
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: goodbye-kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-kubernetes
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello-kubernetes

実行すると下記のような結果になります。

$ conftest test  manifests.yaml  --combine
FAIL - Combined - main - [{"contents":{"apiVersion":"v1","kind":"Service","metadata":{"name":"hello-kubernetes"},"spec":{"ports":[{"port":80,"targetPort":8080}],"selector":{"app":"goodbye-kubernetes"},"type":"LoadBalancer"}},"path":"manifests.yaml"},{"contents":{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"hello-kubernetes"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"hello-kubernetes"}}}},"path":"manifests.yaml"}]

出力結果のjsonを整形してあげると下記のようになります。
それぞれのResourceが配列となっています。ドキュメントにあるようにpathキーとcontentsキーをそれぞれのアイテムが持っています。pathキーはファイル名でcontentsは実際のドキュメントの中身です。

 [{
     "contents": {
         "apiVersion": "v1",
         "kind": "Service",
         "metadata": {
             "name": "hello-kubernetes"
         },
         "spec": {
             "ports": [{
                 "port": 80,
                 "targetPort": 8080
             }],
             "selector": {
                 "app": "goodbye-kubernetes"
             },
             "type": "LoadBalancer"
         }
     },
     "path": "manifests.yaml"
 }, {
     "contents": {
         "apiVersion": "apps/v1",
         "kind": "Deployment",
         "metadata": {
             "name": "hello-kubernetes"
         },
         "spec": {
             "replicas": 3,
             "selector": {
                 "matchLabels": {
                     "app": "hello-kubernetes"
                 }
             }
         }
     },
     "path": "manifests.yaml"
 }]

複数ドキュメントを横断してチェックしてみる

inputのファイルなどはまとめてこちらのリポジトリに置いてあります。

github.com

最初の方でも言及したようにServiceリソースの .spec.selectorが Deploymentの.spec.template.labelsに含まれるかをチェックしてみたいと思います。
※ ここではlabelのkeyをappとします

package main

deny[msg] {
    input[deploy].contents.kind == "Deployment"
    deployment := input[deploy].contents

    input[svc].contents.kind == "Service"
    service := input[svc].contents

    service.spec.selector.app != deployment.spec.template.metadata.labels.app
    msg := sprintf("Labels are different! Deployment %v has 'app: %v', Service %v has 'app: %v'", [deployment.metadata.name, deployment.spec.template.metadata.labels.app, service.metadata.name, service.spec.selector.app])
}

ちなみにconftestのドキュメントのExampleにあったように実行すると、下記のようなエラーが出てしまいました。同名の変数ではまずいのではと思い変更して実行したところ上手くいきました。
※ Issue上げたところ対応してもらったので現在はドキュメント上直っていると思います。

Error: running test: load: loading policies: get compiler: 1 error occurred: policy/combine.rego:5: rego_compile_error: var deployment referenced above

これで実行すると下記のようになります。
期待したチェックが機能しています。

$ conftest test  manifests.yaml 
FAIL - Combined - main - Labels are different! Deployment hello-kubernetes has 'app: hello-kubernetes', Service hello-kubernetes has 'app: goodbye-kubernetes'

Rego言語は今まで学習してきた言語とだいぶ違うので感覚を掴みづらいですね…!ただめっちゃ便利なので今後も上手くつかって円滑にチェックしていこうと思います。