Config Connectorを使用し作成したGoogle Cloudリソースを監査するOpen Policy Agent Gatekeeperポリシーの書き方（テストとデバッグ）

#はじめに
Config Connectorを使用し作成したGoogle Cloudリソースを監査するOpen Policy Agent Gatekeeperポリシーに対するテストとデバッグのやり方について記載したいと思います。

#テストの対象とやり方
Open Policy Agent Gatekeeperポリシーにおけるテストは、Constraint templateのRegoで記載したロジック部分をテスト
することになります。
Constraint template自体は、yamlファイルになりますが、テスト対象はRegoの部分のみなになります。そのため、テストを実施するためにRegoの部分のみを記載したファイルを作成し、テストを行います。
Rego自体のテストは、opaコマンドを使用することでテストすることができます。
opaコマンドのダウンロード方法は、以下に記載があります。
https://www.openpolicyagent.org/docs/latest/#1-download-opa

Constraint templateをKubernetesクラスタにデプロイするには、例えばRego単体でテストを実施した後に、テスト済みのRegoのコードを、Constraint templateに追記するスクリプトを準備しておて、CIの中でConstraint template内にコピーするといったことが考えられます。

##ディレクトリ構成の例
こちらがテストを想定したディレクトリ構成の例になります。
constraintディレクトリ配下に、Regoの記述がないcontraint templateとcontraintを配置します。
regoディレクトリ配下に、Regoもコードとテストコードを配置します。

```
├── contraint
│   └── gcpstorageaclconstraintv1
│       ├── contraint.yaml
│       └── template.yaml
└── rego
    └── gcpstorageaclconstraintv1
        ├── src.rego
        └── test_src.rego
```

##テストの実施方法

Regoとテストコードがあるディレクトリ(「.」)を指定するか、または、ファイル自体を引数に指定します。

```
$ opa test -v .
data.gcpstorageaclconstraintv1.test_success: PASS (539.122µs)
data.gcpstorageaclconstraintv1.test_fail: PASS (285.274µs)
--------------------------------------------------------------------------------
PASS: 2/2
```

```
$ opa test -v src.rego test_src.rego
data.gcpstorageaclconstraintv1.test_success: PASS (434.206µs)
data.gcpstorageaclconstraintv1.test_fail: PASS (174.118µs)
--------------------------------------------------------------------------------
PASS: 2/2
```

#実際のテストの書き方

前回同様にPublic bucketを禁止する(allUsersに許可を付与するのを禁止する)constraint templateを使用してテストを記載してみます。
https://qiita.com/atsumjp/items/518b07c5470d51106349

Regoで記載した制約の内容は以下になります。

```
package gcpstorageaclconstraintv1

disallowPublicBucket(reviewEntity) {
  entities := input.parameters.entity
  satisfied := [ good | entity = entities[_]
                  good = lower(entity) == lower(reviewEntity)]
  any(satisfied)
}

violation[{"msg": msg}] {
  entity := input.review.object.spec.entity
  disallowPublicBucket(entity)
  msg := sprintf("Entity is <%v>", [entity])
}
```

テスト自体もRegoで記述します。各テストのルールは先頭に「test_」をつけます。
以下の例では、test_successとtest_failの2つのテストを記載しています。

```
package gcpstorageaclconstraintv1

test_success {
  input := {
    "review": {"object": {"spec": {"entity": "allAuthenticatedUsers"}}},
    "parameters": {"entity": ["allUsers"]}
  }

  result := violation with input as input
  count(result) == 0
}


test_fail {
  input := {
    "review": {"object": {"spec": {"entity": "allUsers"}}},
    "parameters": {"entity": ["allUsers"]}
  }

  result := violation with input as input
  count(result) == 1
}
```

test_successは、config connecterで作成するGCPリソースとしては、「allAuthenticatedUsers」を指定しており、constraintでは「AllUsers」を指定しているケースをテストしてます。
値が一致しないので、violationルールは、falseを返すことが想定されます。「count(result) == 0」

test_failは、config connecterで作成するGCPリソースとしては、「AllUsers」を指定しており、constraintでは「AllUsers」を指定しているケースをテストしてます。
値が一致する（制約に抵触する）ので、violationルールは、trueを返すことが想定されます。「count(result) == 1」

実際にテストを実施するとテストが成功します。

```
$ opa test -v .
data.gcpstorageaclconstraintv1.test_success: PASS (509.065µs)
data.gcpstorageaclconstraintv1.test_fail: PASS (360.046µs)
--------------------------------------------------------------------------------
PASS: 2/2
```

試しにtest_successの結果を1に変更してみます。「count(result) == 1」

```
test_success {
  input := {
    "review": {"object": {"spec": {"entity": "allAuthenticatedUsers"}}},
    "parameters": {"entity": ["allUsers"]}
  }

  result := violation with input as input
  count(result) == 1
}
```

test_successテストが失敗します。

```
$ opa test -v .
FAILURES
--------------------------------------------------------------------------------
data.gcpstorageaclconstraintv1.test_success: FAIL (566.14µs)

  query:1              Enter data.gcpstorageaclconstraintv1.test_success = _
  test_src.rego:3      | Enter data.gcpstorageaclconstraintv1.test_success
  src.rego:10          | | Enter data.gcpstorageaclconstraintv1.violation
  src.rego:3           | | | Enter data.gcpstorageaclconstraintv1.disallowPublicBucket
  src.rego:7           | | | | Fail any(satisfied)
  src.rego:12          | | | Fail data.gcpstorageaclconstraintv1.disallowPublicBucket(entity)
  test_src.rego:10     | | Fail __local13__ = 1
  query:1              | Fail data.gcpstorageaclconstraintv1.test_success = _

SUMMARY
--------------------------------------------------------------------------------
data.gcpstorageaclconstraintv1.test_success: FAIL (566.14µs)
data.gcpstorageaclconstraintv1.test_fail: PASS (376.568µs)
--------------------------------------------------------------------------------
PASS: 1/2
FAIL: 1/2
```

##CIのやり方
テストの実施から、Constraint templateのapplyまでの流れを考えてみます。

###前提
テスト自体はRego単体で行うため、前述の通りRegoのコードとテストコードが配置されるディレクトリとconstraint templateが配置されるディレクトリを分けます。

```
├── contraint
│   └── gcpstorageaclconstraintv1
│       ├── contraint.yaml
│       └── template.yaml
└── rego
    └── gcpstorageaclconstraintv1
        ├── src.rego
        └── test_src.rego
```

###1.テスト実施
opa testを実行してテストをします。

```
opa test -v .
```

###2.constraint templateを作成する
テストが成功したら、Regoのコードを、constraint templateにコピーします。

```
なんらかのスクリプトを記述して実行
```

###3.constraint templateをデプロイする
kubectlコマンドで、constraint templateをデプロします。

```
kubectl applyy -f contraint.yaml
```

#デバッグ方法
Regoは正直デバッグしづらいと感じています。（何か他に良い方法があれば教えていただきたいです！）

##Rego playgound

Rego playgoundは、Regoをブラウザで実行できるWebサービスです。
https://play.openpolicyagent.org/
Regoのコードとインプットを記載して、EvaluateをクリックすることでOutputが確認できます。
ただ、Pritfデバッグができないのでそういったことがやりたい場合は、ビルトインのtrace関数とopa testコマンドで実施できます。

##ビルトインのtrace関数
printfデバッグは、ビルトインのtrace関数で実施可能です。
trace()関数で、配列のsatisfiedの値を出力してみます。

```
$ cat src.rego
package gcpstorageaclconstraintv1

disallowPublicBucket(reviewEntity) {
  entities := input.parameters.entity
  satisfied := [ good | entity = entities[_]
                  good = lower(entity) == lower(reviewEntity)]
  trace(sprintf("Debug: %v", [satisfied]))
  any(satisfied)
}
```

opa testは、テストが失敗したルールの詳細ログを出力してくれます。
Note "Debug: [false]"と表示があるので、テスト対象のInputがAllUsersにマッチしなかった事がわかります。

```
$ opa test -v .
FAILURES
--------------------------------------------------------------------------------
data.gcpstorageaclconstraintv1.test_success: FAIL (587.654µs)

  query:1              Enter data.gcpstorageaclconstraintv1.test_success = _
  test_src.rego:3      | Enter data.gcpstorageaclconstraintv1.test_success
  src.rego:11          | | Enter data.gcpstorageaclconstraintv1.violation
  src.rego:3           | | | Enter data.gcpstorageaclconstraintv1.disallowPublicBucket
  src.rego:7           | | | | Note "Debug: [false]"
  src.rego:8           | | | | Fail any(satisfied)
  src.rego:13          | | | Fail data.gcpstorageaclconstraintv1.disallowPublicBucket(entity)
  test_src.rego:10     | | Fail __local14__ = 1
  query:1              | Fail data.gcpstorageaclconstraintv1.test_success = _

SUMMARY
--------------------------------------------------------------------------------
data.gcpstorageaclconstraintv1.test_success: FAIL (587.654µs)
data.gcpstorageaclconstraintv1.test_fail: PASS (384.455µs)
--------------------------------------------------------------------------------
PASS: 1/2
FAIL: 1/2
```

##opa run
他の方法だと、opa runでステップ実行することも可能ですが、複雑なロジックになると1行ずつ入力するのは大変になる印象です。
https://www.openpolicyagent.org/docs/latest/#3-try-opa-run-interactive

```
$ opa run
OPA 0.27.1 (commit e514c03, built at 2021-03-12T14:53:58Z)

Run 'help' to see a list of commands and check for updates.

> entity := "allUsers"
Rule 'entity' defined in package repl. Type 'show' to see rules.
> entity
"allUsers"
```

##opa eval
evalコマンドで評価することもできます。

```
$ opa eval "1*2+3"
{
  "result": [
    {
      "expressions": [
        {
          "value": 5,
          "text": "1*2+3",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}
```

```
$ cat input.json
{
  "review": {"object": {"spec": {"entity": "allUsers"}}},
  "parameters": {"entity": ["allUsers"]}
}
```

```
$ opa eval -i input.json -d src.rego "data.gcpstorageaclconstraintv1.violation"
{
  "result": [
    {
      "expressions": [
        {
          "value": [
            {
              "msg": "Entity is \u003callUsers\u003e"
            }
          ],
          "text": "data.gcpstorageaclconstraintv1.violation",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}
```