Posted at

GoogleのContainer Structure TestsをMacOSで実行してみる

More than 1 year has passed since last update.

先日Googleから Container Structure Tests: Unit Tests for Docker Images | Google Open Source Blog というお話が出ていました。

やってみよう。

テストできるのは次のパターン。


  • コマンド実行結果

  • ファイルの場所とパーミッション

  • ファイルの中身

  • コンテナイメージのメタデータ(EXPOSEとかWORKDIRなど)

  • 含まれるOSSのライセンス (GCP上で動かしてもよいかのチェックなのかな?とりあえずdebianのみ対応)

内容からして、ファイルの記述が正しければ動作も正しいだろう、という観点で使うもののようですね。 Structureっつってんだからそういうもんでしょう。


MacOSでのセットアップ

配布しているバイナリはLinuxでのみ動くよ、とのことです。コンテナを配布しているのでそっちを使ってみましょう。

コンテナはこちらに。gcr.io/gcp-runtimes/container-structure-test

$ docker pull gcr.io/gcp-runtimes/container-structure-test:v0.1.3

v0.1.3: Pulling from gcp-runtimes/container-structure-test
cd2f5b7886e9: Pull complete
9be56f236f9a: Pull complete
Digest: sha256:15ae6f3996ebc05b657a042bd5dd8f4d4d66b015effea5fd024c70b8a1f9c42a
Status: Downloaded newer image for gcr.io/gcp-runtimes/container-structure-test:v0.1.3

ヘルプを出してみます。

$ docker run gcr.io/gcp-runtimes/container-structure-test:v0.1.3 -h

Usage of /go_default_test:
-driver string
driver to use when running tests (default "docker")
-image string
path to test image
-pull
force a pull of the image before running tests
-save
preserve created containers after test run
-test.bench regexp
run only benchmarks matching regexp
-test.benchmem
print memory allocations for benchmarks
-test.benchtime d
run each benchmark for duration d (default 1s)
-test.blockprofile file
write a goroutine blocking profile to file
-test.blockprofilerate rate
set blocking profile rate (see runtime.SetBlockProfileRate) (default 1)
-test.count n
run tests and benchmarks n times (default 1)
-test.coverprofile file
write a coverage profile to file
-test.cpu list
comma-separated list of cpu counts to run each test with
-test.cpuprofile file
write a cpu profile to file
-test.memprofile file
write a memory profile to file
-test.memprofilerate rate
set memory profiling rate (see runtime.MemProfileRate)
-test.mutexprofile string
write a mutex contention profile to the named file after execution
-test.mutexprofilefraction int
if >= 0, calls runtime.SetMutexProfileFraction() (default 1)
-test.outputdir dir
write profiles to dir
-test.parallel n
run at most n tests in parallel (default 3)
-test.run regexp
run only tests and examples matching regexp
-test.short
run smaller test suite to save time
-test.timeout d
fail test binary execution after duration d (0 means unlimited)
-test.trace file
write an execution trace to file
-test.v
verbose: print additional output

よしよし。


サンプルのテストを実行する

リポジトリにテスト用のファイルがあるので、それを使ってみましょう。container-structure-test/tests/debian_test.yaml

メタデータ以外のテストが網羅されていますね。

まずMacOSでdockerドライバ(default)をつかってみます。docker.sockをマウントすればOKでしょうきっと。コンフィグファイルもマウントしましょう、ルート直下でOK。

このとき-pullをつけておくとイメージタグが必須になり(-pullでなければローカルからlatestを探索)、テストの前にpullしてくれます。

$ docker run --rm \

-v /var/run/docker.sock:/var/run/docker.sock \
-v `pwd`/debian_test.yaml:/debian_test.yaml \
gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
-pull \
-test.v \
--image gcr.io/google-appengine/debian8:latest \
debian_test.yaml

latest: Pulling from google-appengine/debian8
Digest: sha256:412ef4d53215ff4a95d275ad48fe5196cb51f4f96b99c05058054b3bdf9443c1
Status: Image is up to date for gcr.io/google-appengine/debian8:latest
Using driver docker
=== RUN TestAll
=== RUN TestAll/Command_Test:_apt-get
2018/01/11 06:40:59 Running tests for file debian_test.yaml
=== RUN TestAll/Command_Test:_apt-config
=== RUN TestAll/Command_Test:_path
=== RUN TestAll/File_Existence_Test:_Root
=== RUN TestAll/File_Existence_Test:_Netbase
=== RUN TestAll/File_Existence_Test:_Machine_ID
=== RUN TestAll/File_Content_Test:_Debian_Sources
=== RUN TestAll/File_Content_Test:_Retry_Policy
=== RUN TestAll/Metadata_Test
=== RUN TestAll/License_Test_#0
--- PASS: TestAll (36.99s)
--- PASS: TestAll/Command_Test:_apt-get (1.25s)
docker_driver.go:74: stdout: apt 1.0.9.8.4 for amd64 compiled on Dec 11 2016 09:48:19
Usage: apt-get [options] command
apt-get [options] install|remove pkg1 [pkg2 ...]
apt-get [options] source pkg1 [pkg2 ...]

apt-get is a simple command line interface for downloading and
installing packages. The most frequently used commands are update
and install.

Commands:
update - Retrieve new lists of packages
upgrade - Perform an upgrade
install - Install new packages (pkg is libc6 not libc6.deb)
remove - Remove packages
autoremove - Remove automatically all unused packages
purge - Remove packages and config files
source - Download source archives
build-dep - Configure build-dependencies for source packages
dist-upgrade - Distribution upgrade, see apt-get(8)
dselect-upgrade - Follow dselect selections
clean - Erase downloaded archive files
autoclean - Erase old downloaded archive files
check - Verify that there are no broken dependencies
changelog - Download and display the changelog for the given package
download - Download the binary package into the current directory

Options:
-h This help text.
-q Loggable output - no progress indicator
-qq No output except for errors
-d Download only - do NOT install or unpack archives
-s No-act. Perform ordering simulation
-y Assume Yes to all queries and do not prompt
-f Attempt to correct a system with broken dependencies in place
-m Attempt to continue if archives are unlocatable
-u Show a list of upgraded packages as well
-b Build the source package after fetching it
-V Show verbose version numbers
-c=? Read this configuration file
-o=? Set an arbitrary configuration option, eg -o dir::cache=/tmp
See the apt-get(8), sources.list(5) and apt.conf(5) manual
pages for more information and options.
This APT has Super Cow Powers.
--- PASS: TestAll/Command_Test:_apt-config (1.98s)
docker_driver.go:74: stdout: APT "";
APT::Architecture "amd64";
APT::Build-Essential "";
APT::Build-Essential:: "build-essential";
APT::Install-Recommends "1";
APT::Install-Suggests "0";
APT::NeverAutoRemove "";
APT::NeverAutoRemove:: "^firmware-linux.*";
APT::NeverAutoRemove:: "^linux-firmware$";
APT::VersionedKernelPackages "";
APT::VersionedKernelPackages:: "linux-image";
APT::VersionedKernelPackages:: "linux-headers";
APT::VersionedKernelPackages:: "linux-image-extra";
APT::VersionedKernelPackages:: "linux-signed-image";
APT::VersionedKernelPackages:: "kfreebsd-image";
APT::VersionedKernelPackages:: "kfreebsd-headers";
APT::VersionedKernelPackages:: "gnumach-image";
APT::VersionedKernelPackages:: ".*-modules";
APT::VersionedKernelPackages:: ".*-kernel";
APT::VersionedKernelPackages:: "linux-backports-modules-.*";
APT::VersionedKernelPackages:: "linux-tools";
APT::Never-MarkAuto-Sections "";
APT::Never-MarkAuto-Sections:: "metapackages";
APT::Never-MarkAuto-Sections:: "restricted/metapackages";
APT::Never-MarkAuto-Sections:: "universe/metapackages";
APT::Never-MarkAuto-Sections:: "multiverse/metapackages";
APT::Never-MarkAuto-Sections:: "oldlibs";
APT::Never-MarkAuto-Sections:: "restricted/oldlibs";
APT::Never-MarkAuto-Sections:: "universe/oldlibs";
APT::Never-MarkAuto-Sections:: "multiverse/oldlibs";
APT::AutoRemove "";
APT::AutoRemove::SuggestsImportant "false";
APT::Update "";
APT::Update::Post-Invoke "";
APT::Update::Post-Invoke:: "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";
APT::Architectures "";
APT::Architectures:: "amd64";
APT::Compressor "";
APT::Compressor::. "";
APT::Compressor::.::Name ".";
APT::Compressor::.::Extension "";
APT::Compressor::.::Binary "";
APT::Compressor::.::Cost "1";
APT::Compressor::gzip "";
APT::Compressor::gzip::Name "gzip";
APT::Compressor::gzip::Extension ".gz";
APT::Compressor::gzip::Binary "gzip";
APT::Compressor::gzip::Cost "2";
APT::Compressor::gzip::CompressArg "";
APT::Compressor::gzip::CompressArg:: "-9n";
APT::Compressor::gzip::UncompressArg "";
APT::Compressor::gzip::UncompressArg:: "-d";
APT::Compressor::bzip2 "";
APT::Compressor::bzip2::Name "bzip2";
APT::Compressor::bzip2::Extension ".bz2";
APT::Compressor::bzip2::Binary "false";
APT::Compressor::bzip2::Cost "3";
APT::Compressor::xz "";
APT::Compressor::xz::Name "xz";
APT::Compressor::xz::Extension ".xz";
APT::Compressor::xz::Binary "false";
APT::Compressor::xz::Cost "4";
APT::Compressor::lzma "";
APT::Compressor::lzma::Name "lzma";
APT::Compressor::lzma::Extension ".lzma";
APT::Compressor::lzma::Binary "false";
APT::Compressor::lzma::Cost "5";
APT::Compressor::lzma::CompressArg "";
APT::Compressor::lzma::CompressArg:: "--suffix=";
APT::Compressor::lzma::CompressArg:: "-9";
APT::Compressor::lzma::UncompressArg "";
APT::Compressor::lzma::UncompressArg:: "--suffix=";
APT::Compressor::lzma::UncompressArg:: "-d";
Dir "/";
Dir::State "var/lib/apt/";
Dir::State::lists "lists/";
Dir::State::cdroms "cdroms.list";
Dir::State::mirrors "mirrors/";
Dir::State::extended_states "extended_states";
Dir::State::status "/var/lib/dpkg/status";
Dir::Cache "var/cache/apt/";
Dir::Cache::archives "archives/";
Dir::Cache::srcpkgcache "";
Dir::Cache::pkgcache "";
Dir::Etc "etc/apt/";
Dir::Etc::sourcelist "sources.list";
Dir::Etc::sourceparts "sources.list.d";
Dir::Etc::vendorlist "vendors.list";
Dir::Etc::vendorparts "vendors.list.d";
Dir::Etc::main "apt.conf";
Dir::Etc::netrc "auth.conf";
Dir::Etc::parts "apt.conf.d";
Dir::Etc::preferences "preferences";
Dir::Etc::preferencesparts "preferences.d";
Dir::Etc::trusted "trusted.gpg";
Dir::Etc::trustedparts "trusted.gpg.d";
Dir::Bin "";
Dir::Bin::methods "/usr/lib/apt/methods";
Dir::Bin::solvers "";
Dir::Bin::solvers:: "/usr/lib/apt/solvers";
Dir::Bin::dpkg "/usr/bin/dpkg";
Dir::Bin::bzip2 "/bin/bzip2";
Dir::Bin::xz "/usr/bin/xz";
Dir::Bin::lzma "/usr/bin/lzma";
Dir::Media "";
Dir::Media::MountPath "/media/apt";
Dir::Log "var/log/apt";
Dir::Log::Terminal "term.log";
Dir::Log::History "history.log";
Dir::Ignore-Files-Silently "";
Dir::Ignore-Files-Silently:: "~$";
Dir::Ignore-Files-Silently:: "\.disabled$";
Dir::Ignore-Files-Silently:: "\.bak$";
Dir::Ignore-Files-Silently:: "\.dpkg-[a-z]+$";
Dir::Ignore-Files-Silently:: "\.save$";
Dir::Ignore-Files-Silently:: "\.orig$";
Dir::Ignore-Files-Silently:: "\.distUpgrade$";
Acquire "";
Acquire::cdrom "";
Acquire::cdrom::mount "/media/cdrom/";
Acquire::Retries "3";
Acquire::GzipIndexes "true";
Acquire::CompressionTypes "";
Acquire::CompressionTypes::Order "";
Acquire::CompressionTypes::Order:: "gz";
Acquire::Languages "";
Acquire::Languages:: "none";
DPkg "";
DPkg::Pre-Install-Pkgs "";
DPkg::Pre-Install-Pkgs:: "/usr/sbin/dpkg-preconfigure --apt || true";
DPkg::Post-Invoke "";
DPkg::Post-Invoke:: "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";
CommandLine "";
CommandLine::AsString "apt-config dump";
--- PASS: TestAll/Command_Test:_path (1.54s)
docker_driver.go:74: stdout: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
--- PASS: TestAll/File_Existence_Test:_Root (0.67s)
--- PASS: TestAll/File_Existence_Test:_Netbase (0.16s)
--- PASS: TestAll/File_Existence_Test:_Machine_ID (0.16s)
--- PASS: TestAll/File_Content_Test:_Debian_Sources (0.14s)
--- PASS: TestAll/File_Content_Test:_Retry_Policy (0.18s)
--- PASS: TestAll/Metadata_Test (0.00s)
--- PASS: TestAll/License_Test_#0 (30.90s)
licenses.go:70: acl
licenses.go:70: adduser
licenses.go:70: apt
licenses.go:70: base-files
licenses.go:70: base-passwd
licenses.go:70: bash
licenses.go:70: bsdutils
licenses.go:70: ca-certificates
licenses.go:70: coreutils
licenses.go:70: dash
licenses.go:70: debconf
licenses.go:70: debian-archive-keyring
licenses.go:70: debianutils
licenses.go:70: diffutils
licenses.go:70: dmsetup
licenses.go:70: dpkg
licenses.go:70: e2fslibs
licenses.go:70: e2fsprogs
licenses.go:70: findutils
licenses.go:70: gcc-4.8-base
licenses.go:70: gcc-4.9-base
licenses.go:70: gnupg
licenses.go:70: gpgv
licenses.go:70: grep
licenses.go:70: gzip
licenses.go:70: hostname
licenses.go:70: init
licenses.go:70: initscripts
licenses.go:70: insserv
licenses.go:70: libacl1
licenses.go:70: libapt-pkg4.12
licenses.go:70: libattr1
licenses.go:70: libaudit-common
licenses.go:70: libaudit1
licenses.go:70: libblkid1
licenses.go:70: libbz2-1.0
licenses.go:70: libc-bin
licenses.go:70: libc6
licenses.go:70: libcap2
licenses.go:70: libcap2-bin
licenses.go:70: libcomerr2
licenses.go:70: libcryptsetup4
licenses.go:70: libdb5.3
licenses.go:70: libdebconfclient0
licenses.go:70: libdevmapper1.02.1
licenses.go:70: libgcrypt20
licenses.go:70: libgpg-error0
licenses.go:70: libkmod2
licenses.go:70: liblocale-gettext-perl
licenses.go:70: liblzma5
licenses.go:70: libmount1
licenses.go:70: libpam-modules
licenses.go:70: libpam-modules-bin
licenses.go:70: libpam-runtime
licenses.go:70: libpam0g
licenses.go:70: libpcre3
licenses.go:70: libprocps3
licenses.go:70: libreadline6
licenses.go:70: libselinux1
licenses.go:70: libsemanage-common
licenses.go:70: libsemanage1
licenses.go:70: libsepol1
licenses.go:70: libslang2
licenses.go:70: libsmartcols1
licenses.go:70: libss2
licenses.go:70: libssl1.0.0
licenses.go:70: libsystemd0
licenses.go:70: libtext-charwidth-perl
licenses.go:70: libtext-iconv-perl
licenses.go:70: libtext-wrapi18n-perl
licenses.go:70: libtinfo5
licenses.go:70: libudev1
licenses.go:70: libusb-0.1-4
licenses.go:70: libustr-1.0-1
licenses.go:70: libuuid1
licenses.go:70: login
licenses.go:70: lsb-base
licenses.go:70: mawk
licenses.go:70: mount
licenses.go:70: multiarch-support
licenses.go:70: ncurses-base
licenses.go:70: ncurses-bin
licenses.go:70: netbase
licenses.go:70: openssl
licenses.go:70: passwd
licenses.go:70: perl
licenses.go:70: procps
licenses.go:70: readline-common
licenses.go:70: sed
licenses.go:70: sensible-utils
licenses.go:70: startpar
licenses.go:70: systemd
licenses.go:70: systemd-sysv
licenses.go:70: sysv-rc
licenses.go:70: sysvinit-utils
licenses.go:70: tar
licenses.go:70: tzdata
licenses.go:70: util-linux
licenses.go:70: zlib1g
structure_test.go:47: Total tests run: 10
PASS

最後にPASSでおわればOKです。-test.vをつけない場合はPASSだけでます。FAIL時はexit_codeもちゃんと0以外で終わるので安心。


metadataTestもやってみよう

サンプルにはmetadataTestが入ってませんでした。imageをinspectして、定義をちょろっと書いてみます。


metatest.yaml

schemaVersion: '2.0.0'

metadataTest:
env:
- key: "PORT"
value: "8080"
entrypoint: []
cmd: ["/bin/sh", "-c", "/bin/bash"]

ファイル指定を変更し、実行します。

$ docker run --rm \

-v /var/run/docker.sock:/var/run/docker.sock \
-v `pwd`/metatest.yaml:/metatest.yaml \
gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
-pull \
-test.v \
--image gcr.io/google-appengine/debian8:latest \
metatest.yaml

latest: Pulling from google-appengine/debian8
Digest: sha256:412ef4d53215ff4a95d275ad48fe5196cb51f4f96b99c05058054b3bdf9443c1
Status: Image is up to date for gcr.io/google-appengine/debian8:latest
Using driver docker
=== RUN TestAll
=== RUN TestAll/Metadata_Test
2018/01/11 06:54:58 Running tests for file metatest.yaml
--- PASS: TestAll (0.01s)
--- PASS: TestAll/Metadata_Test (0.00s)
structure_test.go:47: Total tests run: 1
PASS

無事にテストできたようですね。


driverにtarを指定、 dockerdなしでのチェック

さて、冒頭にもリンクをおいたGoogleの記事では、dockerdがいなくてもテストできるぞ!と書いてあったので-driver tarを指定、dockerのソケットを外してみます。

$ docker run --rm \

-v `pwd`/debian_test.yaml:/debian_test.yaml \
gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
-driver tar \
-test.v \
--image gcr.io/google-appengine/debian8:latest \
debian_test.yaml

Using driver tar
=== RUN TestAll
2018/01/11 06:44:51 Running tests for file debian_test.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry

-- 省略
--- FAIL: TestAll (19.89s)
--- FAIL: TestAll/Command_Test:_apt-get (10.03s)
tar_driver.go:66: Tar driver is unable to process commands, please use a different driver
--- PASS: TestAll/Metadata_Test (9.86s)
structure_test.go:47: Total tests run: 1

commandTestsは流石にダメっぽいです。commandTestsのセクションをコメントアウトして、もう一度実行。

Using driver tar

=== RUN TestAll
2018/01/11 07:18:55 Running tests for file debian_test.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN TestAll/File_Existence_Test:_Root
time="2018-01-11T07:19:04Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:04Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest743647843 from system"
=== RUN TestAll/File_Existence_Test:_Netbase
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
time="2018-01-11T07:19:14Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:14Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest188699238 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN TestAll/File_Existence_Test:_Machine_ID
time="2018-01-11T07:19:24Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:24Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest359712397 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN TestAll/File_Content_Test:_Debian_Sources
time="2018-01-11T07:19:34Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:34Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest785687176 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
=== RUN TestAll/File_Content_Test:_Retry_Policy
time="2018-01-11T07:19:44Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T07:19:44Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest267774023 from system"
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry

... 省略

licenses.go:70: startpar
licenses.go:70: systemd
licenses.go:70: systemd-sysv
licenses.go:70: sysv-rc
licenses.go:70: sysvinit-utils
licenses.go:70: tar
licenses.go:70: tzdata
licenses.go:70: util-linux
licenses.go:70: zlib1g
structure_test.go:47: Total tests run: 7
PASS

PASSしました。テストごとにファイルを展開削除しているようで、これはとても時間がかかる。

metadataTestはどうなるんだろう。

$ docker run --rm \

-v `pwd`/metatest.yaml:/metatest.yaml \
gcr.io/gcp-runtimes/container-structure-test:v0.1.3 \
-driver tar \
-test.v \
--image gcr.io/google-appengine/debian8:latest \
metatest.yaml

Using driver tar
=== RUN TestAll
=== RUN TestAll/Metadata_Test
2018/01/11 06:56:09 Running tests for file metatest.yaml
Retrieving image gcr.io/google-appengine/debian8:latest from source Cloud Registry
time="2018-01-11T06:56:19Z" level=info msg="Finished prepping image gcr.io/google-appengine/debian8:latest"
time="2018-01-11T06:56:19Z" level=info msg="Removing image filesystem directory /tmp/gcr.iogoogle-appenginedebian8latest068032231 from system"
--- PASS: TestAll (10.13s)
--- PASS: TestAll/Metadata_Test (10.13s)
structure_test.go:47: Total tests run: 1
PASS

お、ちゃんとできるね。commandTestsは邪道感があるので、これでよいとするのが理想ではありますね。普通にCIを動かすLinux環境ならファイルの展開削除も早かろうし。


おわりに

コンテナ特化で、いつものコマンド+簡潔な結果期待の形式で読みやすいYamlでかけてチェックできるので良いとおもいます。