Helmのtemplateでループ内から変数を参照する

環境

  • helm v3.8.0

背景

こんな感じで、ほぼ同じ内容のマニフェストを複数作りたいとする。 当然 values.yaml で変数を管理したいし、2つ分コピペするのではなくループでうまいこと処理したい。

# targetgroupbinding.yaml
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: my-target-group-binding-1
spec:
  serviceRef:
    name: awesome-service
    port: 8080
  targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz"
---
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: my-target-group-binding-2
spec:
  serviceRef:
    name: awesome-service
    port: 8081
  targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz"

values.yaml がこんな感じだとする。

# values.yaml
appName: awesome-service

service:
  foo:
    id: 1
    port: 8080
    targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz"
  bar:
    id: 2
    port: 8081
    targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz"

パターン1:ループ中にyaml本体を直接書く場合

サンプル全体: https://github.com/ser1zw/helm-loop-demo/tree/main/demo1

普通にループで処理するなら、TargetGroupBindingyamlはこんな感じ。

# targetgroupbinding.yaml
{{- range .Values.service }}
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: my-target-group-binding-{{ .id }}
spec:
  serviceRef:
    name: {{ $.Values.appName }}
    port: {{ .port }}
  targetGroupARN: {{ quote .targetGroupARN }}
---
{{- end -}}

{{- range .Values.service }} ~ {{- end -}} の間ではコンテキストが .Values.service のそれぞれの値になるので(後述の「参考:ループ内のコンテキストの値」参照)、.Values.service.* 配下の値は .id, port, .targetGroupARN のように直接参照できる。 逆に values.yaml で定義した appName.Values.appName では参照できない。この場合は、ルートコンテキストを指すグローバル変数 $ を使い、 $.Values.appName のように参照する。

参考:ループ内のコンテキストの値

{{- range .Values.service }} ~ {{- end -}} 内でのコンテキストはこんな感じのmapになっている

map[id:1 port:8080 targetGroupARN:arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz]
map[id:2 port:8081 targetGroupARN:arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz]

ちなみにこんな感じで表示して確認できる。

{{- range .Values.service }}
ctx: {{ . }}
{{- end -}}

パターン2:テンプレートを使う場合

サンプル全体: https://github.com/ser1zw/helm-loop-demo/tree/main/demo2

さまざまな事情により、yaml本体をループ中に直接書くのではなく、名前付きテンプレートにしたいということもよくある。

こうする。

# targetgroupbinding.yaml
{{- range .Values.service }}
{{- $ctx := (merge (dict "rootCtx" $) .) -}} # <-- ポイント(1)
{{ template "target-group-binding-template" $ctx }}
{{- end -}}
# _helpers.tpl
{{- define "target-group-binding-template" }}
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: my-target-group-binding-{{ .id }}
spec:
  serviceRef:
    name: {{ .rootCtx.Values.appName }} # <-- ポイント(2)
    port: {{ .port }}
  targetGroupARN: {{ quote .targetGroupARN }}
---
{{- end }}

テンプレートは {{ template テンプレート名 コンテキスト }} で呼び出せるが、コンテキストは1つしか渡せない。 そこで ポイント(1) のように、ループ時点でのルートコンテキスト $ とループ内のコンテキスト . をマージした上で渡してやる。 ここではルートコンテキストのキーを rootCtx にしているが、これは何でもよい。

こうすることで、テンプレート内ではループ内のコンテキストの値は普通に .id のように参照できるし、元のルートコンテキストの値は ポイント(2) のように rootCtx を使って .rootCtx.Values.appName のように参照できる。

ちなみに今回は template を使っているが、include でも同じ。

ダメな例

公式ドキュメントだと template の呼び出し時のコンテキストに . を渡す例しかないので、そのまま . を渡しがち。

# targetgroupbinding.yaml
{{- range .Values.service }}
{{ template "target-group-binding-template" . }} # <-- これ
{{- end -}}

で、テンプレート内でルートコンテキストを参照するために $ を使おうとすると…

# _helpers.tpl
{{- define "target-group-binding-template" }}
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: my-target-group-binding-{{ .id }}
spec:
  serviceRef:
    name: {{ $.Values.appName }} # <-- これ
    port: {{ .port }}
  targetGroupARN: {{ quote .targetGroupARN }}
---
{{- end }}

こんな感じでエラーになる。

Error: template: demo/templates/_helpers.tpl:8:14: executing "target-group-binding-template" at <$.Values.appName>: nil pointer evaluating interface {}.appName

Use --debug flag to render out invalid YAML

なんでかというと、template にループ内のコンテキスト . を渡しているので、テンプレート内では ルートコンテキスト = ループ内のコンテキスト になってしまっているため。

参考

UnicodeエスケープされたJavaプロパティファイルをsedとnkfで読めるようにする

native2ascii無き現代に.propertiesの化石と向き合う

もう2022年なのでnative2asciiはJDKに同梱されなくなっているわけですが、一方で目の前にはUnicodeエスケープされた.propertiesファイルが。

手元にはnative2asciiなんぞ無いので、sednkfでなんとかしましょう。

環境

  • macOS Big Sur 11.6
    • brew install nkfnkfを入れてある

こうする

$ cat example.properties | sed -E 's/\\u([0-9A-z]{4})/\&#x\1;/g' | nkf --numchar-input

例えばこんな感じの example.properties なら…

$ cat example.properties
# example.properties
FOO=\u3075\u30FC
BAR=\u3070\u30FC

こうなる

$ cat example.properties | sed -E 's/\\u([0-9A-z]{4})/\&#x\1;/g' | nkf --numchar-input
# example.properties
FOO=ふー
BAR=ばー

ポイント

Unicodeエスケープされた文字列のデコードには、nkf--numchar-input オプションが使える。 しかしこのコマンドで受け付けるのは &#XXXX; のような形式。 Javaのプロパティファイルは \uXXXX の形式でエンコードされているので、sed\uXXXX&#xXXXX; に置換してから nkf に渡す。

混在する改行コードをBSD版sedで変換する

環境

CR+LF, CR, LF を全て LF に統一したい

改行コード CR+LF, CR, LF が混在した邪悪なファイル file.txt がある。

$ echo -n 'あいうえお\r\nかきくけこ\rさしすせそ\n' > file.txt
# file.txt
あいうえお<CR+LF>
かきくけこ<CR>
さしすせそ<LF>

これをmacOS標準のBSDsed コマンドで LF に統一したい。

こうする。

$ sed -e ':L' -e 'N' -e '$!bL' -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

file.txt 全体を読み込んでから CR+LFLF に置換し、最後に残った CRLF に置換する。ファイル全体を読み込むので、ファイルサイズが大きい場合は注意。

仕組み

処理の流れは下記のとおり。

  1. :L: ラベル L を定義する。
  2. N: ファイルから次の行を読み込み、パターンスペース(sedが読み込んだ行を保存する領域)に追加する。
  3. $!bL: ファイル最終行 $ でない ! 場合は分岐(ジャンプ) b する。分岐先はラベル L*1
  4. s/\r\n/\n/g: パターンスペース内で \r\n\n に置換する。
  5. s/\r/\n/g: パターンスペース内で \r\n に置換する。

1.〜3.のループにより、ファイル全体がパターンスペースに保存される。 4.〜5.では、パターンスペースに保存されたファイル全体の文字列に対して、それぞれ CR+LFLF, CRLF を行っている。

後述のとおり、通常sedは1行ずつの読み込み時に改行コード LF が含まれないため、CR+LFの行が /\r\n/ にはマッチしない。 しかし2.のようにNコマンドで読み込むことで、改行コード LF も含めてパターンスペースに保存されるため /\r\n/CR+LF の行にマッチするようになり、CR+LFLF の置換ができる。

※後から気づいたんだけどこっちのが楽

$ sed -e 's/\r$//g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

CR+行末(つまり CR+LF での改行)の CR を削除してから、残りの CRLF に置換する。

後述のとおり、sedの出力時にはもともと行末についていた LF も出てくるので、行末の CR を削除してしまえば自動的に LF での改行として出力される。 あとは CR で改行されているものだけ LF に置換すればよい。

うまくいかない例

単純に CR を削除する

$ sed -e 's/\r//g' file.txt
あいうえお
かきくけこさしすせそ

よくあるやつ。CR+LFLF に変換するだけであればOKだが、CR のみの場合は改行コードが消えてしまうので今回の条件ではNG。

行ごとに CR+LF でマッチさせようとする

$ sed -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

通常 sed は1行ごとに文字列を処理していくが、そのときの処理対象の文字列には改行コード LF は含まれないため、's/\r\n/\n/g' では CR+LF にマッチしない。結局 's/\r/\n/g' しか処理されないので、CR+LFLF が二重になってしまう。

(例えば あいうえお\r\n という文字列は、sed コマンドで普通に読み込まれると あいうえお\r に見えており、\n が含まれない)

行ごとに CR+行末 でマッチさせようとする

$ sed -e 's/\r$/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

\r$CR+LF の行にマッチするものの、出力時には置換後の \n に加えて、もともと行末についていた \n も出てくるため、改行コードが二重になってしまう。

*1:つまりファイル最終行に到達するまで、1.に戻って、2.で次の行を読み込んで、…を繰り返す

Spring Bootでプロパティを環境変数から読むときの変換ルール

これは完全に自分用メモなのですが

Spring Bootでプロパティの値を環境変数から読むときの変数名の変換ルールは下記のとおり。

To convert a property name in the canonical-form to an environment variable name you can follow these rules:

  • Replace dots (.) with underscores (_).
  • Remove any dashes (-).
  • Convert to uppercase.

For example, the configuration property spring.main.log-startup-info would be an environment variable named SPRING_MAIN_LOGSTARTUPINFO.

Environment variables can also be used when binding to object lists. To bind to a List, the element number should be surrounded with underscores in the variable name.

For example, the configuration property my.service[0].other would use an environment variable named MY_SERVICE_0_OTHER.

docs.spring.io

tr コマンドで tr 'a-z.[' 'A-Z__' | tr -d ']-' とすれば変換できる。

$ echo 'spring.main.log-startup-info' | tr 'a-z.[' 'A-Z__' | tr -d ']-'
SPRING_MAIN_LOGSTARTUPINFO
$ echo 'my.service[0].other' | tr 'a-z.[' 'A-Z__' | tr -d ']-'
MY_SERVICE_0_OTHER

環境

  • Spring Boot 2.6.2

findコマンドの-depthオプションのGNU版とBSD版の違いでハマったという話

なにごと?

find コマンドで「1階層目のファイル/ディレクトリだけを列挙したい」となり*1macOS$ man find したら -depth オプションを使えばよさそうだったので、こんなふうにしてたわけです。

$ find . -depth 1

で、これをGitHub ActionsでやっているCI(ubuntu-latestなイメージ)に組み込んだら、こんなエラーが。

find: paths must precede expression: `1'

よく見てみると、findコマンドのオプションがBSD版とGNU版で異なるためにエラーになっている様子。 というわけで、ちゃんとマニュアルを読んでみる。

環境

BSD版の -depth

-depth n

True if the depth of the file relative to the starting point of the traversal is n.

https://www.freebsd.org/cgi/man.cgi?find(1)

つまり、指定したパスの開始点から深さ n のファイルやディレクトリが列挙される。

ちなみに引数 n を取らない -depth オプションもある。

-depth

Always true; same as the non-portable -d option. Cause find to perform a depth-first traversal, i.e., directories are visited in post-order and all entries in a directory will be acted on before the directory itself. By default, find visits directories in pre-order, i.e., before their contents. Note, the default is not a breadth-first traversal.

https://www.freebsd.org/cgi/man.cgi?find(1)

こっちは深さ優先探索にするオプションらしい。

GNU版の -depth

-depth

ディレクトリそのものより先に、ディレクトリの中身を処理する。 アクション -delete を使用すると、-depth オプションも自動的に設定される。

https://linuxjm.osdn.jp/html/GNU_findutils/man1/find.1.html

つまり、BSD版の -depth (引数を取らないほう)と同じく深さ優先探索になる。 引数を取る -depth n は無い。このため、前述のエラーになっていたということらしい。

GNU版でもBSD版の -depth n みたいに特定の階層のみ列挙したい場合は?

-mindepth-maxdepth を組み合わせればよい。 下記のようにすれば、1階層目のファイル/ディレクトリだけが列挙される。

$ find . -mindepth 1 -maxdepth 1

*1:これだけだと「lsでいいじゃん」ってなるのですが、実際にはもうちょっと細かい条件があって、findが適切だったのです

jshellコマンドを使って.javaファイル無しでJavaプログラムを実行する

環境

$ jshell -version
jshell 17

本題

小ネタ。

jshellコマンドは標準入力からソースコードを流し込めるので、シェルのヒアドキュメントを使うことでソースコードのファイルを作らずにJavaプログラムを実行できる。

例えば「CIのときにあらかじめcreate databaseしておきたい!けどpsqlコマンドを使うためだけにapt-get install postgresqlするのもなんかやだ!!手元にJDBCドライバはある!!!」みたいなレアな状況で役に立つ。

$ jshell -s --class-path ~/local/lib/postgresql-42.3.1.jar <<EOS
import java.sql.DriverManager;
try (var c = DriverManager.getConnection("jdbc:postgresql://localhost:5432/", "postgres", "postgres");
    var s = c.createStatement()) {
    s.execute("create database testdb");
}
EOS

CI以外でも、ちょっとしたJavaプログラムをシェルスクリプト内で実行したいときなどに使えるはず。

mvnコマンドに-Dオプションを渡せばpom.xmlのプロパティが指定できる

はじめに

実は知らなかったシリーズ。ドキュメントちゃんと読んでないのがバレる。

環境

本題

例えばこんな感じで pom.xml を書いて…

<properties>
    <someOption>${someValue}</someOption>
</properties>

こんな感じで -D オプション付きで実行する。

$ mvn -DsomeValue=SOME_VALUE

そうすると、pom.xml${someValue}SOME_VALUE がセットされる。コマンドで動作を切り替えたいときとかに使える。

-Dシステムプロパティを定義するオプション。 -Dシステムプロパティを定義して ${ } で参照するという、至極当たり前の話ではある。 当然、<properties> 以外のタグ内でも使える。

ちなみに ${env.XXX}環境変数 XXX の値も参照できるらしい。${env.PATH} とか。

maven.apache.org

参考