cockscomblog?

cockscomb on hatena blog

Development Containersのfeatureを作る

OSによって作られるメタデータファイル(.DS_StoreとかThumbs.dbとか)をgitignoreするとき、プロジェクトじゃなくてグローバルの設定にしたい。それで長年 ~/.config/git/ignore にファイルを置いていた。内容はgithub/gitignoreから取ってくる。giboを使っているなら、gibo dump macOS > ~/.config/git/ignore するだけだ。

Development Container

最近Development Containersを使ってみていて、おおよそ気に入っているのだけど、このグローバルなgitignoreの扱いに悩んだ。手元のファイルシステムからマウントされるので、.DS_StoreファイルがDevelopment Containerの中から見えてしまう。しかしグローバルなgitignoreは(あえてマウントしなければ)設定されていないから、gitの差分に出てきてしまう。

もちろんプロジェクトの .gitignore ファイルに書いたらいいのだけど、どうも気乗りしない。ということで、Development Containersのfeatureとして作ってみる。

Featureとは

Development Containersについて何年前かに使ったときは、このfeatureという概念がなかったように思う。コンテナに何か追加したければDockerfileを書くような感じだった。ところが最近では、Development Containerにfeatureを適用することで、必要な機能を追加する。例えばコンテナにNode.jsを入れたければ、.devcontainer/devcontainer.jsonfeaturesghcr.io/devcontainers/features/nodeを書き加える。

  "features": {
    "ghcr.io/devcontainers/features/node:1": {}
  },

このように、featureはOCI Imageとしてパッケージングされ、配布される。Node.jsのfeatureはfeatures/src/node at main · devcontainers/features · GitHubでその実態を見られる。

Featureを作る

自分でfeatureを作るのは、テンプレートリポジトリから始めるのがいちばん良さそうだ。GitHub Actionsもよく整備されている。サンプルとなるfeatureとして、colorhelloが入っている。これを真似していく。

まずテンプレートリポジトリから自分のリポジトリを作る。ひとつのリポジトリで複数のfeatureを提供するのが普通なようだ。このリポジトリ自体がDevelopment Containerで開発するようになっているので、VS Codeからコンテナで開く。

src/以下にディレクトリを作って、devcontainer-feature.jsoninstall.shを置く。サンプルではREADME.mdもあるが、これは後から自動生成されるので、自分で作る必要がない。

src
└── gitignoreglobal
    ├── README.md
    ├── devcontainer-feature.json
    └── install.sh

devcontainer-feature.jsonの仕様に合わせて書けばよい。optionsを定義しておくと、featureへの入力として文字列か真偽値を得られる。

install.shの方が本体で、ここにシェルスクリプトを書く。これはrootとして実行される。Development Containerとして実行する際は、例えばvscodeユーザーなどで実行されるので、その差に注意が要る。実際、gitignoreglobal featureではsystemのgit configを書き換えることにした。あまり上品ではないが、後から作られるユーザーのことを知る由もないので、諦めた。

#!/bin/sh
set -e

GITIGNORE_PATH="$(git config --system --get core.excludesfile || true)"
if [ -z "${GITIGNORE_PATH}" ]; then
  GITIGNORE_PATH=/etc/gitignore
  git config --system --add core.excludesfile $GITIGNORE_PATH
fi
echo "Using global gitignore file: ${GITIGNORE_PATH}"

mkdir -p "$(dirname "${GITIGNORE_PATH}")"
curl -sS "https://raw.githubusercontent.com/github/gitignore/main/${GITIGNORE}.gitignore" >> "${GITIGNORE_PATH}"

optionsで設定した入力値は環境変数として渡されるので、$GITIGNOREとしてこれを使っている。

curlgitdevcontainer-feature.jsondependsOnを設定していることで使えている。

    "dependsOn": {
        "ghcr.io/devcontainers/features/common-utils": {}
    }

テスト

テンプレートリポジトリから始めると、test/にテストが入っている。scenarios.jsonにテストシナリオを書いて、キー名と一致するkeyname.shに実際のテストを書く。test.shはデフォルトのテストということに決まっている。

test/gitignoreglobal
├── macos.sh
├── scenarios.json
└── test.sh

今回はscenarios.jsonで、macOS用のテストを定義する。

{
    "macos": {
        "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
        "features": {
            "gitignoreglobal": {
                "gitignore": "Global/macOS"
            }
        }
    }
}

macos.shは次のように書いた。source dev-container-features-test-libすると、checkreportResultsが使えるようになって、非常に便利。

#!/bin/bash

set -e

source dev-container-features-test-lib

# check <LABEL> <cmd> [args...]
mkdir -p /tmp/test_1
cd /tmp/test_1
git init

check "no difference at start" [ -z "$(git status --porcelain)" ]

touch .DS_Store
check "no difference after adding .DS_Store" [ -z "$(git status --porcelain)" ]

touch NOT_IGNORED
check "difference after adding NOT_IGNORED" [ -n "$(git status --porcelain)" ]

reportResults

実行するにはdevcontainer CLIを使う。devcontainer features test --features gitignoreglobalのようにすると、特定のfeatureのテストを実行できる。Docker in Dockerで、新しいコンテナの中で実行されるので、やろうと思えば複数のベースイメージに対してテストを実行させられる。

またGitHub Actionsでも実行されるようになっている。

デプロイ

デプロイもdevcontainer CLIでできるが、事前に設定されたGitHub Actionsでやるのが簡単だった。GitHub Packagesにデプロイできる。README.mddevcontainer-feature.jsonの内容から自動的に生成され、Pull Requestが作られるので、便利だ。

  "features": {
    "ghcr.io/cockscomb/devcontainer-features/gitignoreglobal:1": {
      "gitignore": "Global/macOS"
    }
  },

書いていて気がついたけど、macOSWindowsが混在している環境だったら複数指定したいと思う。options環境変数マッピングされる都合からか、文字列と真偽値しか受け付けないので、スペース区切りで"Global/macOS Global/Windows"のようにできるとよさそう。

ひとまずDevelopment Containersのfeatureを作ってみた。テンプレートリポジトリやCLIが整備されているおかげで、普通にやるとテストも書けるし、CI/CDも用意できる。OCI Imageとして配布されるのもハイテクな感じがする。全体的によくできたエコシステムと思う。