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

前編はこちら

bufferings.hatenablog.com

CUE で YAML をマージするときのルールを自分の言葉で説明すると以下の通りだった

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

今回は、どうしてそうなるのか?について簡単にメモを残しておこうと思う

いったん CUE 形式に変換される

YAML を マージするときに僕が使っているのは、こういうコマンド

❯ cue export a.yml b.yml --out yaml

流れはこう

  1. YAML ファイルを CUE 形式に変換する
  2. 変換した複数の CUE 形式のデータをマージ
  3. マージされた CUE 形式のデータを YAML で出力

コマンドの最後につけてる --out yaml をはずすと、マージされたものが CUE 形式のまま出力される

❯ cue export a.yml b.yml
{
    "sample": {
        "foo": "foo value",
        "bar": "bar value"
    }
}

ここで少し CUE について見てみる

CUE は型と値を区別せずに扱う

CUE には型と値を区別せずに扱うという特徴があり、通常なら値を書く部分に、型や条件を書くこともできる

foo: {
    name: string
    count: < 10
}

この例は

  • foo: name: の値は文字列でなければならない
  • foo: count: の値は10未満の数値でなければならない

という制約を表している。そして、foo: name:foo: count: には、これらの条件を満たす値しか設定できない

CUE では同じ要素を複数回定義することができて、その場合はお互いに矛盾しないかの検査が行われる

なので、こういう定義は条件が矛盾しないので OK で

foo: {
    name: string
    count: < 10
}

foo: {
    name: "orange"
    count: 2
}

値も指定してあるので export するとこうなる

❯ cue export foo.cue
{
    "foo": {
        "name": "orange",
        "count": 2
    }
}

でも、たとえば、10以上の値を count に指定すると矛盾するのでエラーになる

foo: {
    name: string
    count: < 10
}

foo: {
    name: "orange"
    count: 10
}
❯ cue export foo.cue
foo.count: invalid value 10 (out of bound <10):
    ./foo.cue:3:9
    ./foo.cue:8:9

だから、CUE を使うと、条件を指定したスキーマを用意しておいて、実際の値がそれらの条件を満たしているかを検査することができる

そして、ひとつのパスに対して、複数の異なる値が指定されると、それらの値が矛盾してしまうので、エラーになる

foo: {
    name: "orange"
}

foo: {
    name: "apple"
}
❯ cue export foo.cue 
foo.name: conflicting values "apple" and "orange":
    ./foo.cue:2:8
    ./foo.cue:6:8

CUE の仕様はなんかもっと色々あって、Lattice 束 (そく)とか出てきて、僕はよくわかってない!のだけど、YAML のことを考えるのには、もうこれだけ分かってたら十分

ちなみに、書いてある順番は関係ないので、先に値があって、その次に条件が書いてあっても動きは同じになる

YAML には値しかない

YAML の値の部分に指定されたものは、値として扱われて、型にはならないので、こう書いても↓

foo:
  name: string

文字列という型にはならずに "string" という値が指定されていることになる。だから、↓のような YAML を CUE に変換すると

foo:
  name: orange
  count: 5

こうなって

{
    "foo": {
        "name": "orange",
        "count": 5
    }
}

もう、これで foo: name:foo: count: の値は決定してしまい、これ以外の値を同じパスに指定することはできない。だから、↓これは CUE に変換するときにエラーになる

foo:
  name: orange
  count: 5
---
foo:
  count: 6
❯ cue export a.yml
foo.count: conflicting values 6 and 5:
    ./a.yml:3:11
    ./a.yml:6:11

という CUE の仕様から、YAML をマージするときは、こういう動きになるのだった

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

おしまい

おまけ:CUE の機能を利用してみる

読み込むファイルは YAML と CUE を混ぜても別に問題ないので、YAML をマージしつつ、CUE で要素を差し込むなんてこともできる

a.yml

version: 2.1

jobs:
  service1-say-hello:
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

b.yml

version: 2.1

jobs:
  service2-say-hello:
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!2"

これに CUE ファイルとしてこんな c.cue を混ぜると。。。

jobs: {
    [string]: {
        docker:
        [{image: "cimg/base:stable"}]
    }
}

すべての job に差し込むことができる

❯ cue export a.yml b.yml c.cue --out yaml
version: 2.1
jobs:
  service1-say-hello:
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
    docker:
      - image: cimg/base:stable
  service2-say-hello:
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!2
    docker:
      - image: cimg/base:stable

面白いね

でも、んー、僕はたぶん使わないかなぁ。YAML 同士のマージならまだ雰囲気で使えるけど、CUE の機能を使うとなると CUE の仕様を把握する必要があるし、最初に書いた人は大丈夫だけど、そのうち誰も分からないけど動いてる・・・ってなりそうだから(じゃあこんな機能を紹介しなくていいのでは!?

これで一連の CircleCI の設定ファイル分割の話と、CUE の話はおしまい!楽しかったー!

水曜日の勉強会で

このあたりのことを実際にお見せしますー!よかったら来てね

circleci.connpass.com