deck利用時のJSON Pathの書き方

deckの一部のファイル操作(`patch`、`add-plugins`等)では対象エンティティの選択に[JSON Path](https://github.com/json-path/JsonPath)形式を利用する。（deckの`patch`や`add-plugins`の説明には書いていないが、[Linting](https://docs.konghq.com/deck/file/lint/)のページに記載がある）
JSON Pathの書き方は[RFC-9535](https://www.rfc-editor.org/rfc/rfc9535.html)で規定されていて、基本的にはここで定義された書き方で書けるが、例外もある（後述）。
ここではdeckで使えるJSON Pathの書き方を`deck file add-plugins`コマンドを使って検証する。

検証では以下のファイルを用いた。
```yaml:base.yaml
_format_version: "3.0"
services:
- enabled: true
  host: httpbin.org
  name: httpbin-service-1
  path: /
  port: 443
  protocol: https
  tags:
  - httpbin-1
  routes:
  - name: httpbin-route-1
    paths:
    - /httpbin-1
    protocols:
    - https
- enabled: true
  host: httpbin.konghq.com
  name: httpbin-service-2
  path: /
  port: 443
  protocol: https
  tags:
  - httpbin-2
  routes:
  - name: httpbin-route-2
    headers:
      foo:
      - bar
    paths:
    - /httpbin-2
    protocols:
    - https
```

add-pluginsで使うYAMLは以下をベースとする。
```yaml:key-auth.yaml
_format_version: "1.0"
add-plugins:
- overwrite: false
  plugins:
  - name: key-auth
  selectors:
  - $
```
この`selectors`でJSONPath形式でクエリが書けるので、これを色々変えて`base.yaml`のどこに`key-auth.yaml`で定義したプラグインを差し込めるかを検証する。

### Global Scopeで追加
まず、元のkey-auth.yamlの`selectors`であるが、`$`がルートを意味するため、Global Scopeで適用される。
適用した結果はこちら。
```yaml
$ deck file add-plugins -s base.yaml key-auth-global.yaml
_format_version: "3.0"
plugins:
- name: key-auth
services:
- enabled: true
：（省略）
```
Service等、他のエンティティには適用されず最上位に挿入される。

### Serviceへの追加
#### 全Serviceを対象
全てのServiceに適用する場合は以下のような感じ。
```yaml
  selectors:
  - $.service[*]
```
`.`でファイル内の子要素を参照する。`[]`で配列のアクセスとなり、`*`を指定することで全ての配列の要素を参照する感じになる。
実行した結果は以下となる。
```yaml
_format_version: "3.0"
services:
- enabled: true
  host: httpbin.org
  name: httpbin-service-1
  path: /
  plugins:
  - name: key-auth
  port: 443
　：（省略）
- enabled: true
  host: httpbin.konghq.com
  name: 
  
  path: /
  plugins:
  - name: key-auth
  port: 443
　：（省略）
```

#### 名前が一致するServiceに適用
Service名を指定する場合は以下。
```yaml
- selectors:
  - $.services[?(@.name=='httpbin-service-2')]
```
`?()`でフィルタを指定する。
フィルタ内では`@`で現在の要素を指定、その中の`.name`が`'httpbin-service-2'`と一致するものを絞って選択している。
結果は以下。
```yaml
services:
- enabled: true
  host: httpbin.org
  name: httpbin-service-1
  ：（省略）
- enabled: true
  host: httpbin.konghq.com
  name: httpbin-service-2
  path: /
  plugins:
  - name: key-auth
  port: 443
```
`name ==`で選択した`httpbin-service-2`のみに適用された。

なお、`selectors:`は複数書けるようになっているが、複数書いた場合はOR条件になる。
なので、複数のService名を指定したい場合は以下のように書く。
```yaml
- selectors:
  - $.services[?(@.name=='httpbin-service-1')]
  - $.services[?(@.name=='httpbin-service-2')]
```

#### タグが一致するServiceに適用
`deck gateway dump`した後に、特定のタグのものにPluginを追加したいようなケースとかで実行する。
以下のような感じで書く。
```yaml
- selectors:
  - $.services[?(@.tags[?(@ == 'httpbin-1')])]
```
先程の`name`は単純な一致だったが、`tags`は配列で複数書けるので、`tags[]`の配列のキーを比較する形で書く。

#### 名前とタグが一致するServiceに適用
フィルタ内にAND条件を追加する。AND条件は`&&`で書けて、名前一致とタグ一致はこれまで検証した式がそのまま使えるので、`&&`で結合する。
```yaml
- selectors:
  - $.services[?(@.name == 'httpbin-1' && @.tags[?(@ == 'httpbin-1')])]
```
### Routeへの追加
#### 全Routeを対象
```yaml
- selectors:
  - $..routes[*]
```
`$.services[*].routes[*]` でもいいが、`..`だとどこに`routes`がいても設定してくれる。Serviceに紐づかないようなRouteがいるようなケースでも使える。
逆にServiceに紐づくRouteのみ指定したい場合は`$.services[*].routes[*]` の方がいいと思う。
#### パスが一致するRouteに適用
いちいちRouteの名前なんて覚えていないが、パス名は分かるからそこにプラグイン差し込みたい、というような人向け。
```yaml
- selectors:
  - $.services[*].routes[?(@.paths[?(@ == '/httpbin-1')])]
```
`$..routes[?(@.paths[?(@ == '/httpbin-1')])]`で行けそうなものだが、deckの不具合（正確には[使っているライブラリの不具合](https://github.com/vmware-labs/yaml-jsonpath/issues/54)）で適用できない。
なので、回避策として`..`を使わずに指定する。

#### ヘッダが一致するRouteに適用
ヘッダはdeckだと`headers.key[].value`のような構造になっているため、以下のような形になる。

```yaml
- selectors:
  - $.services[*].routes[?(@.headers.foo[?(@ == "bar")])]
```

#### パスとヘッダが一致するRouteに適用
同じパスにアクセスさせるのだけど、ヘッダが違う時だけ振る舞いを変えるようなケース。
```yaml
- selectors:
  - $.services[*].routes[?(@.paths[?(@ == '/httpbin-2')] && @.headers.foo[0] == 'bar']
```
これもバグがあり、`$.services[*].routes[?(@.paths[?(@ == '/httpbin-2')] && @.headers.foo[?(@ == "bar")])]`だと上手く行かない（[JSONPath Online Evaluator](https://jsonpath.com/)では通る）。
どうもフィルタを複数通したあとに`&&`を使って更にフィルタを使うと構文エラーとされてしまう模様。
なので苦肉の策としてヘッダ側は配列決め打ちでフィルタを使わないようにする。

## 結論
JSONPathで柔軟にKongのエンティティを選択してプラグインの追加等が出来ることが確認できた。
検証ではService、Routeのみ対象としたが、Consumer等も名前が変わるだけで同じやり方で操作できるので、JSONPathを使ったselectorを活用することでdeck利用時に柔軟にYAMLをカスタマイズ出来るかと思う。
一方で、ライブラリのバグで上手く動かないケースもそこそこあることが確認できた。
つかっていライブラリがBroadcomに買収された[VMware製](https://github.com/vmware-labs/yaml-jsonpath)であることを考えるとバグ修正も簡単には行かなさそうな気がするので、利用する場合（特に複雑な式を書く場合）はバグありきで事前検証をしっかりした方が良さそうだ。