Cのプロダクトを作るためにやったこと

今年に入って Octopass このエントリーをはてなブックマークに追加 というプロダクトを公開しました。 それは、Linuxのユーザや権限をGithubのTeamと連携して運用を楽にするというツールでした。

色んな方々のご協力により、多くのRetweetやはてぶいただいたことで、ある程度、 Octopass を必要としそうな人の目に触れたのではないかと思っています。 (Githubのスター数が少ないのは今後の課題) その中で「すごく便利」「ぜひ導入したい」というフィードバックは、 継続して機能追加していくというモチベーションにつながっていて、非常にありがたいです。

さて、この Octopass は、Linuxユーザ名前解決をするためにの glibc の libnssモジュールをCで実装しています。 cgoやその他の言語でShared Objectを吐き出しても良かったのですが、それだと技術的挑戦が足りないとして、触れてこなかったCに挑戦しました。

ただ、リリース当初、認証や公開鍵を取得する部分はgoで書いていて、1プロジェクトの中に2種類の言語を使って成り立っていましたが、 管理が複雑になることと、同様の実装が2つ存在してしまうことが無駄であることを感じて、 v0.2としてgoの部分をCに置き換えるという作業を行い純粋にCのプロダクトとしました。 そのことで、CIをシンプルにでき、類似の実装をしなくて良くなりましたし、見通しがずいぶん良くなった気がします。

今回、Cのプロダクトを作るにあたって、Cを勉強する以外にどんなことをやったか振り返ってみようと思います。

Clang Format

まず、自分が作るのがlibnssのモジュールなので既存のものをgithubで検索してひたすら読んでみました。 最初に気になったのが、coding styleがバラバラなこと! 有名なプロダクトであっても、あまりこれがスタンダードというものがなく、さあ、どうしようという状態になっていました。

これは @matsumotory さんに clang-format という整形ツールがあるよという情報を得ました。

clang-formatを導入すると、基本スタイルというものがあって、 LLVM・Google・Mozilla・Linux… といったそれぞれの流派を選択できる様になっていたのでとても便利でした。

早速適当にformatを定義し(設定は下記)、vimと連携することで保存時にきれいに整形されるという感じになりました。

# requires clang-format >= 3.6
BasedOnStyle: "LLVM"
IndentWidth: 2
ColumnLimit: 120
BreakBeforeBraces: Linux
AllowShortFunctionsOnASingleLine: None
SortIncludes: false
AlignConsecutiveAssignments: true
AlignTrailingComments: true
AllowShortBlocksOnASingleLine: true

また、もし仮に Pull-Requestをもらった時に指定するフォーマットから外れていたら、 私やPull-Requestをくれた方にわかる様、CIで formatのdiffをチェックするようにもしました。

Criterion

次に、Unit Test。色々Cのプロジェクト見ていると、main書いて最低限のテストを書いてたり、 自前でassertion作ってたりといて結構カオスです。 何かしらtest frameworkはないかなと調べると良さそうなものがいくつかあります。

真面目に比較する時間はなかったのでexampleが多いものと出力が分かりやすいものをとりあえず使ってみようと Criterion を使い始めました。

#include <criterion/criterion.h>

Test(suite_1, failing) {
    cr_assert(0);
}

Test(suite_1, passing) {
    cr_assert(1);
}

Criterion は結構便利で 色々な assertion が準備されてるのはもちろんですが、 異常系のテストとして exit codeや signalを検証する仕組みもあります。 これは、Criterion がテストケースのプロセスを分離して実行結果をレポートする仕組みによるものです。 また、Theoryテストを簡単にやれたりテストスイートを楽に設定できるのもメリットなのかなと(他のフレームワークを使って比較してないので)想像しています。

Makefile

次に、テストを頻繁に実行しだすとコンパイルしてテスト実行というのが面倒になってきます。 そうすると GNU Make の出番で、ビルドやビルドに必要な依存ライブラリ等をセットアップすることを定義していきます。 そして、Make の自己文書化という裏技があるようなので導入しました。簡単に言えば Makerake -T みたいなことができます。

pkg: ## Create some distribution packages
	rm -rf builds && mkdir builds
	docker-compose up

dist: ## Upload archives on Mac
	@test -z $(GITHUB_TOKEN) || $(MAKE) github_release
	@test -z $(PACKAGECLOUD_TOKEN) || $(MAKE) packagecloud_release

help:
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(INFO_COLOR)%-30s$(RESET) %s\n", $$1, $$2}'

ターゲット名の後ろにコメントを書くことでそれらがターゲットの一覧となるようにしています。

Packaging

プロダクトが完成すると、使いやすくするために、パッケージにしたくなってきます。 そのパッケージ化を手でやってたら面倒なので docker-compose up で一気に作成されるようにします。

rpm:
  dockerfile: dockerfiles/Dockerfile.centos
  build: .
  volumes:
    - .:/octopass
  command: make rpm
deb:
  dockerfile: dockerfiles/Dockerfile.ubuntu
  build: .
  volumes:
    - .:/octopass
  command: make deb

また、作成済みパッケージをGithub Rereasesにアップロードするのはおなじみ ghr を使い、 PackageCloud へのアップロードは github.com/mlafeldt/pkgcloud を使用しました。 そうすることで、各ディストリビューションパッケージの作成公開を make dist というコマンドだけで完結してしまいます。

Integration Test

ユニットテストしか書いてなかったので、いざ作ったパッケージをインストールして試したら Segmentaion fault とか出たりして統合テストが出来てなかったことに気づきBashで書きました。

function test_octopass_passwd() {
  actual="$(/usr/bin/octopass passwd linyows)"
  expected="linyows:x:74049:2000:managed by octopass:/home/linyows:/bin/bash"

  if [ "x$actual" == "x$expected" ]; then
    pass "${FUNCNAME[0]}"
  else
    fail "${FUNCNAME[0]}" "$expected" "$actual"
  fi
}

function run_test() {
  self=$(cd $(dirname $0) && pwd)/$(basename $0)
  tests="$(grep "^function test_" $self | sed -E "s/function (.*)\(\) \{/\1/g")"
  for t in $(echo $tests); do
    $t
  done
}

run_test
exit $ALL_PASSED

今回は統合テストの検証項目が少ないためさっとbashで書いてしまいましたが、Criterion で出来たかもしれません。

Conclusion

Cのプロダクトを作る際にやったことをまとめると以下です。

これらを抽象化すると、Cスターターみたいなものが出来るなと思いつつ、 Criterion のような便利なツール使うだけで、Cの開発苦手だなーという意識が変わって、 随分前向きで楽しくなるものだなというのを改めて思いました。

Cは勉強始めたばかりなので、メモリの気持ちが理解できるようにやっていくぞ!


おまけ

ちなみに、Octopass v0.3では共有ユーザ機能を追加しています。 指定したユーザをチームで共有し認証することができるというものです。 ユースケーストしては、アプリなどの実行またはdeployのためのユーザを共有するのを想定しています。

Octopass はキャッシュすることで、高速さとGithubというSPOFを避けていますが、サーバの台数が大規模になると、 どうしてもGithubのAPI Rate limitを超えAPIの手前にproxyを設置してリクエスト回数を減らすという施策が必要になってきます。 今後は、そのキャッシュをOctopass クラスタ間で共有することでGithub APIのproxyを不要にしようと考えています。 実際にキャッシュを共有するのは、EtcdConsul Key/Value Data とかを使う感じになるイメージです。

· C OSS