CUE で YAML をマージするときの動きを確認

CircleCI の config.yml を分割できる(というかマージできる)Orb を作ったので

bufferings.hatenablog.com

今日は、その Orb の裏側の「CUE で YAML をマージするときの動き」を確認してみるー!

CUE で YAML をマージする

こんなファイルがあるときに

foo.yml

sample:
  foo: "foo value"

bar.yml

sample:
  bar: "bar value"

こう書くと CUE で YAML をマージすることができる

❯ cue export foo.yml bar.yml --out yaml
sample:
  foo: foo value
  bar: bar value

--- を使って、↓こう書いても同じなので、今日の記事の中ではこの形式で書こうと思う

foobar.yml

sample:
  foo: "foo value"
---
sample:
  bar: "bar value"
❯ cue export foobar.yml --out yaml 
sample:
  foo: foo value
  bar: bar value

どうマージされるのか?

とてもシンプルで、これだけだった↓

  • YAMLYAMLのLeafまでのパス: 値 と捉えたときに、それらのすべてのLeafが任意の順番でマージされる
  • ただし、そのLeafに対する値がひとつに決まらなければエラーになる

注意:自分の頭の中の理解を自分の言葉で説明しているので、YAMLのLeafまでのパス: 値 とか Leaf は、正式に定義されている言葉じゃないです

YAMLLeafまでのパス?

こんな YAML があったときに

a: 3
b:
  c: "foo"

こんな風に考えることができる

a: 3
b: c: "foo"

この、a:b: c: のことを YAMLのLeafまでのパス と、この記事の中では呼ぶことにする。3"foo" がそのLeafの値。

パスが違う場合はマージされる

パスが違う場合はマージされる

a: 1
---
b: 2
❯ cue export foobar.yml --out yaml
a: 1
b: 2

階層構造を持っている場合でもパスがそれぞれ違うなら問題なくマージされる

a:
  b: "bbb"
  c: "ccc"
---
a:
  d: "ddd"
❯ cue export foobar.yml --out yaml
a:
  b: bbb
  d: ddd
  c: ccc

値がひとつに決まらなければエラーになる

複数の YAML に同じパスの Leaf が定義してあって、それらの値が異なる場合はエラーになる

a: 1
---
a: 2
❯ cue export foobar.yml --out yaml
a: conflicting values 2 and 1:
    ./foobar.yml:1:5
    ./foobar.yml:3:5

a: 1a: 2 で値がひとつに決まらないからエラーになる

階層構造を持っている場合も同じ

a:
  b: "bbb"
---
a:
  b: "ddd"
❯ cue export foobar.yml --out yaml
a.b: conflicting values "ddd" and "bbb":
    ./foobar.yml:2:7
    ./foobar.yml:5:7

a: b: の値がひとつに決まらなくてエラーになる

値がひとつに決まる場合はエラーにはならない

じゃあ、値がひとつに決まる場合はどうなるの?というと、ひとつの Leaf として出力される

a: 1
---
a: 1
❯ cue export foobar.yml --out yaml
a: 1

階層構造の場合も同じ

a:
  b: "bbb"
---
a:
  b: "bbb"
❯ cue export foobar.yml --out yaml
a:
  b: bbb

値がシーケンスの場合

これまではスカラー値だけを見てきたけど、シーケンス値の場合でも同じで、値が異なる場合にはエラーになるし

a:
  - 1
  - 2
---
a:
  - 1
❯ cue export foobar.yml --out yaml
a: incompatible list lengths (1 and 2)

同じ値ならエラーにはならない

a:
  - 1
  - 2
---
a:
  - 1
  - 2
❯ cue export foobar.yml --out yaml
a:
  - 1
  - 2

シーケンス値を縦に書くと少し分かりにくいので、こう書くと、値ということが分かりやすいかもしれない

a: [1, 2]
---
a: [1]
❯ cue export foobar.yml --out yaml
a: incompatible list lengths (1 and 2)

もちろん、シーケンスが別の Leaf の値であればマージされる

a:
  - 1
  - 2
---
b:
  - 1
❯ cue export foobar.yml --out yaml
a:
  - 1
  - 2
b:
  - 1

つまり「複数のファイルで、同じパスのLeafにそれぞれ異なるシーケンス値を定義していても、それらのシーケンスが結合されることはなく、エラーになる」

マージのルール(再掲)

ということで、とてもシンプルだった:

  • YAMLYAMLのLeafまでのパス: 値 と捉えたときに、それらのすべてのLeafが任意の順番でマージされる
  • ただし、そのLeafに対するスカラー値またはシーケンス値がひとつに決まらなければエラーになる

CircleCI の設定ファイル的にはどうなの?

CircleCI の設定ファイルを、最初に書いた Orb でマージするときにどうなるかというと、例えば↓こういうファイルがあるときに

version: 2.1

jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

workflows:
  service1-say-hello-workflow:
    when: << pipeline.parameters.build-service1 >>
    jobs:
      - service1-say-hello
version: 2.1

jobs:
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!2"

workflows:
  service2-say-hello-workflow:
    when: << pipeline.parameters.build-service2 >>
    jobs:
      - service2-say-hello
  • version: は値がひとつに決まるのでその値が使用される
  • jobs: service1-say-hello:jobs: service2-say-hello: は別のノードになるので、それぞれが jobs の下にぶらさがる
  • workflows も同様

だからマージすると、こうなる

version: 2.1
jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!2
workflows:
  service1-say-hello-workflow:
    when: << pipeline.parameters.build-service1 >>
    jobs:
      - service1-say-hello
  service2-say-hello-workflow:
    when: << pipeline.parameters.build-service2 >>
    jobs:
      - service2-say-hello

同じ名前のジョブやワークフローを書いてしまった場合は?

例えば、間違って複数のファイルに同じ名前のワークフロー定義を書いてしまった場合は、大体の場合は jobs などの値が異なってエラーになると思う

service-say-hello-workflow:
  jobs:
    - service-say-hello
---
service-say-hello-workflow:
  jobs:
    - service-say-hello
    # jobs の内容が違う
    - notify-something

jobs のシーケンス値が異なるので、エラーになる

❯ cue export foobar.yml --out yaml
"service-say-hello-workflow".jobs: incompatible list lengths (1 and 2)

ただ、Leafの内容が全く同じで、別の要素が追加されている場合は、マージされるので注意が必要。↓こういう風に書いてしまった場合

service-say-hello-workflow:
  jobs:
    - service-say-hello
---
service-say-hello-workflow:
  # 要素を追加
  when: << pipeline.parameters.build-service1 >>
  jobs:
    - service-say-hello

マージされてしまう

❯ cue export foobar.yml --out yaml
service-say-hello-workflow:
  when: << pipeline.parameters.build-service1 >>
  jobs:
    - service-say-hello

CircleCI の設定ファイルを分割するときの基本的な方針

基本的な方針としては、各ファイルごとにネームスペース的なプレフィックスを付けるようにして、ジョブやワークフローの名前が重複しないようにするのが良さそう

どういう仕様で、こんな動きをするの?

CUE の仕様について触れる必要があるので、また次回にでもー!

2022-07-25 後編書いた

bufferings.hatenablog.com