🔧 Kustomizeで利用されるRFC6902 JSON Patchを読んでみる

本記事では、Kubernetesのmanifestを管理するためのkustomizeで利用できるpatchesについて、参照されているRFCを読みながら理解することを目指します。

patchesの具体例

まず、patchesの具体的な利用を確認します。
以下のようなIngressにpatchを当てたいとします。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example
spec:
  rules:
  - host: blog.ymgyt.io
    http:
      paths:
      - path: /
        backend:
          serviceName: service-1
          servicePort: 5001

kustomization.yamlは以下のようになります。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ingress.yaml

patches:
- path: ingress_patch.yaml
  target:
    group: networking.k8s.io
    version: v1beta1
    kind: Ingress
    name: example

適用するpatchはingress_patch.yamlに定義します。
意図としては、/ui用のruleを追加することです。

- op: add
  path: /spec/rules/0/http/paths/-
  value:
    path: '/ui'
    backend:
      serviceName: ui
      servicePort: 5002

全体としては以下のような構成です。

> exa -T .
.
├── ingress.yaml
├── ingress_patch.yaml
└── kustomization.yaml

ここで、kustomize build .してみると

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example
spec:
  rules:
  - host: blog.ymgyt.io
    http:
      paths:
      - backend:
          serviceName: service-1
          servicePort: 5001
        path: /
      - backend:
          serviceName: ui
          servicePort: 5002
        path: /ui

Ingressにpatchに定義した/uiのruleが追加できていることが確認できました。

patchの仕様

上記では以下のようなpatchを適用しました。

- op: add
  path: /spec/rules/0/http/paths/-
  value: # ...

ここから、patchにはop,path,valueを指定する必要がありそうなことがわかります。
ただし、opに他にどんな値があるのかであったり、/spec/rules/0/http/paths/--の意味であったりは、kustomizeの公式docには定義されていませんでした。

Referenceによりますと

The patches field contains a list of patches to be applied in the order they are specified.
Each patch may:
* be either a strategic merge patch, or a JSON6902 patch
* be either a file, or an inline string
* target a single resource or multiple resources

とあり、patchはJSON6902で指定することができる
JSON6902とは、patchJson6902を指しており、これはRFC6902を参照しているとありました。

ということで、patchをどう書けばいいかはRFC6902(JSON Patch)を読んでみればよさそうということがわかりました。

RFC6902 JavaScript Object Notation (JSON) Patch

まず、Introductionにて

JSON Patch is a format (identified by the media type "application/ json-patch+json") for expressing a sequence of operations to apply to a target JSON document

JSON PatchはJSONに適用する一連のoperationを表現するためのformatであるとされています。

また、

This format is also potentially useful in other cases in which it is necessary to make partial updates to a JSON document or to a data structure that has similar constraints

とあり、JSONの一部を更新したいユースケースで便利と説明されています。kustomizeでの利用はまさにこのcaseのことでしょうか。

Document structure

Document structureでは、具体例として下記のjsonが挙げられていました。

[
   { "op": "test", "path": "/a/b/c", "value": "foo" },
   { "op": "remove", "path": "/a/b/c" },
   { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
   { "op": "replace", "path": "/a/b/c", "value": 42 },
   { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
   { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

そして、各operationの適用については

  1. operationは定義された順序に従って、適用される
  2. operationが適用された結果に対して、次のoperationが適用される
  3. operationの適用は全て成功するか、errorが発生するまで続く

とされていました。
試しに、意味がないですが、addした結果を直後にremoveしてみたところ意図通りになりました。

- op: add
  path: /spec/rules/0/http/paths/-
  value:
    path: '/ui'
    backend:
      serviceName: ui
      servicePort: 5002

- op: remove
  path: /spec/rules/0/http/paths/1

Operations

Operationsに、具体的なoperationのfieldについて定められています。

Operation objects MUST have exactly one "op" member, whose value indicates the operation to perform.
Its value MUST be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors.

  • operationにはop fieldが必須
  • opの値は"add","remove","replace","move","copy","test"のいずれかでそれ以外はerror

Additionally, operation objects MUST have exactly one "path" member. That member's value is a string containing a JSON-Pointer value [RFC6901] that references a location within the target document (the "target location") where the operation is performed.

  • operationにはpath fieldが必須
  • path fieldでtarget documentの変更適用箇所を指定する

The meanings of other operation object members are defined by operation (see the subsections below).
Members that are not explicitly defined for the operation in question MUST be ignored (i.e., the operation will complete as if the undefined member did not appear in the object).

  • oppath以外のfieldについてはopの値による
  • 定義されていないfieldは無視される

ということで、次に各種opに指定できる値をみていきます。

add

例:

{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }

add operationはpathの指定に応じて以下の3つの効果を及ぼします。

  • If the target location specifies an array index, a new value is inserted into the array at the specified index.
  • If the target location specifies an object member that does not already exist, a new member is added to the object.
  • If the target location specifies an object member that does exist, that member's value is replaced.
  • pathがarray indexなら指定されたindexにvalueをinsertする
  • pathがobject memberの場合、存在しないなら新規作成、存在するならreplace

addですが、既存をreplaceするんですね。
また、追加する値を指定するvalueが必須です。 そして、

The specified index MUST NOT be greater than the number of elements in the array.

とあることから、indexが既存のarrayの長さを超えているとerrorになります。

なお、path: /foo/-のような-ですが

If the "-" character is used to index the end of the array (see [RFC6901]), this has the effect of appending the value to the array.

とされており、arrayの最後の要素を指せるようです。便利ですね。 参照されているRFC6901のJSON Pointer側では

exactly the single character "-", making the new referenced value the (nonexistent) member after the last array element.

と説明されていました。

remove

例:

{ "op": "remove", "path": "/a/b/c" }

The "remove" operation removes the value at the target location.
The target location MUST exist for the operation to be successful.

removeは指定の要素をremoveするのでそのままですね。

If removing an element from an array, any elements above the specified index are shifted one position to the left.

Arrayのelementをremoveした場合は、残りの要素がshiftされます。

replace

例:

{ "op": "replace", "path": "/a/b/c", "value": 42 }

The "replace" operation replaces the value at the target location with a new value.
The operation object MUST contain a "value" member whose content specifies the replacement value.
The target location MUST exist for the operation to be successful.

replaceもadd同様指定の値に置き換えます。
addとの違いはpathで指定した要素が存在しない場合にerrorとなる点にあります。
replaceはremove + addと機能的に同じです。

move

例:

{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" }

moveはfromに対してremoveを行ったのち、removeされた値をaddする動作になります。
自分はkustomizeでは使ったことがありませんでした。

copy

例:

{ "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }

copyはfromで指定された値をpathにaddする動作になります。
これもkustomizeでは使ったことがありませんでしたが、使い所がありそうな気もする。

test

例:

{ "op": "test", "path": "/a/b/c", "value": "foo" }

The "test" operation tests that a value at the target location is equal to a specified value.

testはjsonの値を変更する他のoperationと性格が違うように思われました。
試しにさきほどのpatchで使ってみますと

- op: add
  path: /spec/rules/0/http/paths/-
  value:
    path: '/ui'
    backend:
      serviceName: ui
      servicePort: 5002

- op: test
  path: /spec/rules/0/http/paths/1
  value:
    path: '/foo'   #  👈
    backend:
      serviceName: ui
      servicePort: 5002

としてみると

> kustomize build .
`Error: testing value /spec/rules/0/http/paths/1 failed: test failed`

となり、kustomizeでもサポートされているようでした。

まとめ

  • kustomization.yamlのpatchesに書けるpatchの仕様はRFC6902 JSON Patchに定義されている
  • operationではoppathのfieldが必須となり、その他のfieldはopの値に依る
  • opにはadd, remove, replace, move, copy, testが利用できる

簡単にではありますが、kustomizeのpatchの仕様を確認できました。