Skip to main content

56 posts tagged with "oss"

View All Tags

· 5 min read
Shunsuke Suzuki

buildflow というツールを開発しているので紹介します。 buildflow というタグをつけて何回かに分けてブログを書こうかなと思います。 1本目のこの記事では

  • どんなツールか
  • Hello World
  • 特徴

について簡単に説明します。

どんなツールか

https://github.com/suzuki-shunsuke/buildflow

ワークフローを実行するための CLI ツールです。 ワークフローエンジンと言うと Airflow とか Azkaban, Argo Workflows のようなツールをイメージするかと思いますが、 それらとは目的も機能も違います。 一部の CI サービスではワークフローのローカル実行をサポートしてたりしますが、そんなイメージで良いかもしれません。 buildflow では task と task の依存関係を設定ファイルに定義し、コマンドを実行するとローカルでタスクが実行されます。 そういうとタスクランナーといったほうがいいのかもしれませんが、個別のタスクを指定して実行するような機能はないので、タスクランナーとも違う気がします。

CI サービス上で実行することを目的として開発しています(汎用的なツールなので他の目的でも使えるとは思います)。

Hello World

まだどんなツールかピンと来てない人もいるかもしれないので、簡単な Hello World をやってみましょう。

GitHub Releases からバイナリをダウンロードしてください。

次のような設定ファイル .buildflow.yaml を用意します。

---
phases:
- name: main
tasks:
- name: hello
command:
command: echo hello

次のコマンドを実行すると task が実行されます。

$ buildflow run

==============
= Phase: main =
==============
07:50:46UTC | hello | + /bin/sh -c echo hello
07:50:46UTC | hello |
07:50:46UTC | hello | hello
07:50:46UTC | hello |

================
= Phase Result: main =
================
status: succeeded
task: hello
status: succeeded
exit code: 0
start time: 2020-10-17T07:50:46Z
end time: 2020-10-17T07:50:46Z
duration: 4.317259ms
+ /bin/sh -c echo hello
hello

特徴

task を依存関係に基づいて並列実行できます。加えて以下のような特徴があります。

  • ワンバイナリで動く。他に依存するものがない
    • Go で書かれています
  • 他のタスクの実行結果によってタスクの挙動を変えられる
    • 標準(エラー)出力、 exit code, etc
    • dynamic に task を生成することも可能
  • Tengo というスクリプト言語を用いて柔軟な設定を書ける
    • 設定ファイルは YAML ですが、一部の設定に Tengo というスクリプト言語が使えます
    • Tengo の処理系も buildflow に内包されているので、 Tengo の処理系をインストールする必要はありません
  • CI で実行時に Pull Request (以下 PR) の情報に基づいて処理を変えられる
    • GitHub 前提(GitLab や BitBucket はサポート外)
    • 自動で PR の情報を取得

Tengo は Go で実行されたスクリプト言語です。 https://github.com/d5/tengo を参照してください。 なぜ Tengo を採用したかとかは別に書きたいと思います。

Tengo は Python や Ruby, Go といった言語に比べれば言語仕様がコンパクトであり、 よく知らなくてもなんとなく読めるし、簡単にかけると思っています。 加えて、そもそも buildflow の中での Tengo の役割は限定されており、 Tengo をゴリゴリ書く必要はないと思います。

PR の情報に基づき、例えば以下のようなことが出来ます。

  • 特定のラベルがついたら task を実行する
  • 特定のファイルが PR で更新されたら task を実行する
  • 特定のユーザー(botとか)からの PR では task を実行しない

これらのロジックは自分がよく欲しくなるので、 buildflow を使えば簡単に実装できるようにしました。

他にも様々な機能がありますので、また別の記事で紹介できたらと思います。

· 9 min read
Shunsuke Suzuki

GitHub の issue や pull request, commit にコメントを投稿する CLI ツールを作りました(結構前の話ですが)。

https://github.com/suzuki-shunsuke/github-comment

このブログの執筆時点で最新は v1.5.0 です。

Go 製なので、 GitHub Releases からダウンロードしてくれば簡単にインストールできます。

想定している主な用途は、 CI/CD の 結果をコメントで通知することで DX を向上することです。 例えば CI がこけたらこけたコマンドとエラーメッセージを通知するなどです。

github-comment には

  • init: 設定ファイルの雛形を生成する
  • post: コメントを投稿する
  • exec: 外部コマンドを実行し、その結果を元にコメントを投稿する

という 3 つのサブコマンドがあります。

コメントの投稿には GitHub の Access Token が必要です。 コマンドライン引数 -token でも渡せますが、環境変数として設定しましょう。

$ export GITHUB_TOKEN=xxx # GITHUB_ACCESS_TOKEN も可

post コマンド

こんな感じでコメントを投稿できます。

$ github-comment post -org suzuki-shunsuke -repo github-comment -pr 1 -template test

パラメータの数が多いですが、いくつかの Platform では環境変数から自動でパラメータを補完してくれます。

  • Drone
  • CircleCI
  • GitHub Actions

そうするとこれでよくなります。

$ github-comment post -template test

コメントは Go の text/template で処理されます。 {{.Org}} {{.Repo}} といった感じでパラメータを参照できます。

$ github-comment post -template "{{.Org}}/{{.Repo}} test"
  • PRNumber
  • Org
  • Repo
  • SHA1
  • TemplateKey
  • Vars

exec コマンド

コマンドの実行結果(標準出力、標準エラー出力、 exit code) を元にコメントを投稿したい場合に、 exec コマンドが使えます。

$ github-comment exec -template "{{.ExitCode}} {{.Stdout}}" -- echo hello

コマンドを実行した上でコメントを投稿します。テンプレートの変数として、 post でも渡されるパラメータの他にコマンドの実行結果が渡されます。

  • ExitCode
  • Stdout
  • Stderr
  • CombinedOutput
  • Command: exec.Command.String
  • JoinCommand: コマンド引数(配列)をスペース " " でつないだ文字列

github-comment exec の標準入力は実行するコマンドに渡されますし、 github-comment exec の exit code は実行したコマンドの exit code になります。

設定ファイル

上記の例では投稿するコメントを -template で渡していましたが、ごく短いコメント以外は設定ファイルに記述したほうが良いでしょう。 設定ファイルは github-comment init で雛形を生成できます。

$ github-comment init

こんな感じで複数のテンプレートを記述できます。

post:
default: |
foo
hello: |
hello
...

そして post 実行時に -template-key (-k) で使用するテンプレートを指定します。 -template-k 両方指定しない場合、デフォルトで default テンプレートが使用されます。

$ github-comment -k hello

exec の設定

exec の設定はもう少し複雑です。 それはコマンドの実行結果によって使用するテンプレートを変えたり、あるいはコメントを投稿しなかったりできるようにするためです。 一つのテンプレートキーに対し、複数のテンプレートを配列で設定します。

exec:
default:
- when: ExitCode == 0
template: |
success
- when: ExitCode != 0
template: |
failed
...
...

そしてテンプレート文字列とは別に when という、そのテンプレートを使う条件を設定します。 この条件は antonmedv/expr によって処理されます。 テンプレートの text/template とはまた別の構文なのがややこしいですね。 条件の評価結果は当然真偽値ではないといけません。 評価結果が true ならばそれを使ってコメントし、あとは無視されます。 false なら次のテンプレートを評価します。 全部マッチしなければコメントは投稿されませんし、エラーにもなりません。 dont_comment: true とすると、その条件にマッチした場合はコメントを投稿せずに終了します(後続のテンプレートも無視されます)。

exec:
hello:
- when: ExitCode != 0
dont_comment: true
- when: true
template: |
Hello, world

テンプレートの再利用

templates を使うと複数のテンプレートでヘッダーなどを共通化して再利用できます。

templates:
header: "# {{.Org}}/{{.Repo}}"
post:
default: |
{{template "header" .}}

Go の text/template に馴染みがないとわかりにくいかと思いますが、

templates:
テンプレート名: テンプレート

でテンプレートを定義して

{{template "テンプレート名" .}}

でテンプレートを参照できます。

テンプレートの変数をコマンドライン引数で渡す

-var 変数名:値 でパラメータを渡せます。 {{.Var.変数名}} で参照できます。

$ github-comment post -var name:foo -template "Hello, {{.Var.name}}"

設定ファイルで変数を定義する

設定ファイルで変数を定義できます。任意の型の変数を定義できます。 {{.Var}}.変数名 で参照できます。

vars:
foo: bar
zoo:
foo: hello

{{.Var.zoo.foo}}

post コマンドの標準入力でテンプレートを渡す

github-comment post の標準入力でテンプレートを渡せます。 exec の場合はそうはならない(実行するコマンドに渡される)ので注意してください。

$ echo foo | github-comment post

パラメータの補完

先に述べたとおり、いくつかの Platform では環境変数から自動でパラメータを補完してくれます。

CircleCI

プラットフォームの判別: CIRCLECI の有無

パラメータソース
.OrgCIRCLE_PROJECT_USERNAME
.RepoCIRCLE_PROJECT_REPONAME
.PRNumberCIRCLE_PULL_REQUEST
.SHA1CIRCLE_SHA1

Drone

プラットフォームの判別: DRONE の有無

パラメータソース
.OrgDRONE_REPO_OWNER
.RepoDRONE_REPO_NAME
.PRNumberDRONE_PULL_REQUEST
.SHA1DRONE_COMMIT_SHA1

GitHub Actions

プラットフォームの判別: GITHUB_ACTIONS の有無

パラメータソース
.OrgGITHUB_REPOSITORY
.RepoGITHUB_REPOSITORY
.PRNumberGITHUB_EVENT_PATH
.SHA1GITHUB_SHA1

exec のパラメータの Command と JoinCommand

実行したコマンドを示すパラメータとして Command と JoinCommand があります。 これらは似てますが、微妙に違います。

Command は exec.Cmd の .String() から取得されるのですが、コマンドが絶対パスになったりするので、あまり望ましくないこともあるでしょう。

例えば echo hello の場合 /usr/local/opt/coreutils/libexec/gnubin/echo foo となったりします。

一方 JoinCommand はコマンド文字列を単にスペースでつないだものになります。

exec.Cmd の .String() のドキュメントに書いてあるとおり、 .Command と .JoinCommand はそのまま shell で実行するのには適さない形式なので注意してください。

String returns a human-readable description of c. It is intended only for debugging. In particular, it is not suitable for use as input to a shell. The output of String may vary across Go releases.

設定ファイルのパス

設定ファイルのパスは --config -c オプションで指定できます。 何も指定しない場合、カレントディレクトリからルートディレクトリに向かって .github-comment.yml, .github-comment.yaml を探索し、最初に見つかったものを使います。

設定ファイルで .Org, .Repo を指定する

設定ファイルでコメント先のリポジトリを指定できます。 Platform で補完される場合や、明示的にパラメータで指定する場合は不要です。

base:
org: suzuki-shunsuke
repo: github-comment

· 3 min read
Shunsuke Suzuki

多分車輪の再発明だとは思いますが、簡単にツールをインストールするための CLI ツールを作りました。 tarball や zip をダウンロードして展開して指定したパスにインストールするツールです。

https://github.com/suzuki-shunsuke/clap

Go で書かれています。 ツールの名前(clap)には特別な意味や理由はなく、なんとなくです。

CI で何かしらのツールをインストールすることがままあって、そのためのシェルスクリプトを都度書くのが割と面倒なのでツール化しました。

このブログを書いている時点でバージョンは v0.1.0-1 で、最低限の機能しかありませんが、9割型ニーズを満たせるかなと思います。

使い方は以下のようになっています。

$ clap <URL> <インストールするファイルのアーカイブ内での相対パス>:<インストール先> [<インストールするファイルのアーカイブ内での相対パス>:<インストール先>...]

例えば conftest を /usr/local/bin にインストールする場合次のようになります。

CONFTEST_VERSION=0.18.2
clap install https://github.com/instrumenta/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz conftest:/usr/local/bin/conftest
chmod a+x /usr/local/bin/conftest

パーミッションの付与はやってくれないので必要に応じてやってください。 ファイルの圧縮形式は URL から自動で判別してくれます。

上記の conftest のインストールを今までは次のようなシェルスクリプトを書いていました。

#!/usr/bin/env bash

set -eu

CONFTEST_VERSION=0.18.2

dirpath=$(mktemp -d)
pushd "$dirpath"
TARFILE=conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz
curl -OL https://github.com/instrumenta/conftest/releases/download/v${CONFTEST_VERSION}/${TARFILE}
tar xvzf $TARFILE
mv conftest /usr/local/bin/conftest
chmod a+x /usr/local/bin/conftest
popd
rm -R "$dirpath"

地味に面倒ですね。これをツール毎に書いて、しかも圧縮形式によって微妙に変えないといけません。

clap 自体のインストールはどうするかというと、 GitHub Releases で tarball の他にバイナリ単体でも配布しているので簡単にインストールできます。

CLAP_VERSION=0.1.0-1
curl -L -o /usr/local/bin/clap https://github.com/suzuki-shunsuke/clap/releases/download/v${CLAP_VERSION}/clap_${CLAP_VERSION}_linux_amd64
chmod a+x /usr/local/bin/clap

以上、簡単な紹介でした。

· 7 min read
Shunsuke Suzuki

自作の CLI ツール skaffold-generator の紹介です。 プロトタイピングみたいなノリで半日くらいで割と手早く作れました。 名前が長くて適当なのでもっと良い名前ないかなと思ってます。

Skaffold に欲しい機能がないので補完する感じで作ったのですが、「それ〇〇で出来るよ」とかあったら(GitHub issue とか Twitter で)教えていただけると幸いです。

どんなツールか

設定ファイル skaffold-generator.yaml を監視して変更があったら skaffold.yaml を生成するツールです。設定ファイルでサービスの依存関係を定義できたり、コマンドライン引数で指定したサービス及びそれが依存するサービスに関連した設定だけを使って skaffold.yaml を更新します。 このツールは skaffold.yaml を生成するだけなので実際にアプリケーションをビルド・デプロイするには skaffold と組み合わせて使います。

なぜ作ったか

元々ローカルでアプリケーションを動かしながら開発するために Docker Compose を使ってるリポジトリがあるのですが、それを skaffold に移行出来ないか検証しています。 まだ skaffold を触り始めたばかりで理解が浅いのですが、 本番環境は k8s で動いてるからローカルも k8s で動かせるといいかなと思ったり、あとは変更を検知して自動でビルド・デプロイしてくれたりして便利そうかなと思いました。 まぁ結果的に移行しないことになったとしても、 Skaffold と現状の仕組みについて理解が深まればいいかなくらいのつもりです。

検証の過程で、 以下のようなことが Docker Compose だと出来るけど Skaffold だと難しそうだと思いました。

  • サービスの依存関係を定義すること
    • Skaffold というより k8s の問題かとは思いますが
    • Docker Compose だと依存するものを自動で起動してくれて便利
  • コマンドライン引数で指定したサービスだけ起動すること
    • Skaffold だと skafffold.yaml で定義したものすべてがビルド・デプロイされるという認識

サービスの数が少なければ全部ビルド・デプロイでもいいですが、 マイクロサービスをモノレポで管理しているような場合、 すべてのマイクロサービスをビルド・デプロイするのは無駄が大きかったりします。

そこで skaffold.yaml の元となる設定ファイルを用意し、コマンドライン引数でサービスを指定して必要最小限の skaffold.yaml を生成するツールを作ってみました。

インストール

Go のバイナリをダウンロードしてきてください。 https://github.com/suzuki-shunsuke/skaffold-generator/releases

使い方

使い方は簡単です。サブコマンドもありません。 リポジトリにサンプルがあるのでそれを見ましょう。

まずは skaffold-generator.yaml を用意します。

skaffold-generator.yamlbaseservices からなります。

base は生成される skaffold.yaml のベースとなるものです。 deploy.kubectl.manifestsbuild.artifacts は上書きされるので指定しないでください。

services ではサービスのリストを定義します。 各サービスは以下の属性を持ちます。

  • name: サービス名。コマンドライン引数と depends_on でサービスを指定するのに使う。ユニークにする
  • manifests: skaffold.yaml の deploy.kubectl.manifests
  • artifacts: skaffold.yaml の build.artifacts
  • depends_on: サービスが依存するサービス名のリスト

用意したら skaffold-generator を実行します。 skaffold.yaml が生成(既にあれば上書き)され、 skaffold-generator.yaml の変更を監視した状態になります。

$ skaffold-generator
2020/04/05 18:19:37 start to watch skaffold-generator.yaml

コマンドライン引数でサービス名を指定しない場合、すべてのサービスが skaffold.yaml に反映されます。 別のターミナルで skaffold dev を実行すれば 生成された skaffold.yaml を使ってアプリケーションをビルド・デプロイ出来ます。

$ skaffold dev

skaffold-generator.yaml を変更すれば、その変更を検知し skaffold.yaml が更新され、そして skaffold devskaffold.yaml の変更を検知しアプリケーションがビルド・デプロイされます。

引数無しですべてのサービスをデプロイするとこのツールの意味がないので、コマンドライン引数でサービス名を指定しましょう。

$ skaffold-generator api

こうするとサービス apiapi が依存するサービス(依存関係は再帰的に処理されます)だけが skaffold.yaml に反映されます。 依存関係は循環してても大丈夫です。

使い方は以上です。

Docker Compose みたいにできないこと

Docker Compose みたいに依存関係を定義できるようになりましたが、 Docker Compose みたいにデプロイの順序は考慮されません。 まぁこのツールは skaffold.yaml を生成するだけなので仕方ないですね。

最後に

以上、 skaffold-generator の紹介でした。 まだ作ったばっかで自分でも使えてないので本当に使い物になるのかは分かりませんが、 興味ある人は触ってみてください。

· 4 min read
Shunsuke Suzuki

コマンドの実行時間を Datadog に送る dd-time というツールを作りました。

このツールは circle-dd-bench にインスパイアされていますが、 CircleCI 以外でも需要あると思ったり、他にも幾つか改善したい部分があったので自作することにしました。

circle-dd-bench については circle-dd-bench の作者が書いたブログ https://blog.yuyat.jp/post/circle-dd-bench/ も参考にしてください。

dd-time は Go 製なので GitHub Releases からバイナリをダウンロードしてインストールすれば使えます。

使い方はシンプルで実行時間を計測したいコマンドの前に dd-time -- をつけるだけです。 例えば Docker image のビルドの時間を計測したい場合次のような感じになります。

$ dd-time -t command:docker-build -- docker build .

Datadog の API key を環境変数 DATADOG_API_KEY として設定する必要があります。 こうすると Datadog の Post timeseries points API を使い、command_execution_time というメトリックス名(変更可能)でコマンドの実行時間が送られます。

メトリックスの名前や host, tags はそれぞれ --metric-name (-m), --host, --tag (-t) で指定できます。 --tag は複数回指定可能で、 key:value というフォーマットで指定します。

CircleCI で実行した場合、 CircleCI のビルドイン環境変数が tag として勝手に設定されますが、 CircleCI 以外でも使えます。

dd-time を作る上で意識したことは、透過的にする(元のコードにほとんど影響を与えずに使えるようにする)ということです。 具体的には以下のような点です。

  • 標準入力をそのままコマンドに渡す
  • コマンドの標準出力・標準エラー出力をそのまま出力する
  • コマンドの exit code をそのまま dd-time の exit code とする
  • Datadog への送信に失敗しても dd-time の exit code は 0 とする (option で non zero にもできるようにするのもありだが、現状はそうしてない)
  • Datadog への送信に失敗した場合のエラーメッセージをファイルに吐き出せる(コマンドの出力と混ざらないようにできる)
    • デフォルトは標準エラー出力だが、 --output (-o)--apend (-a) オプションで変更できる
    • --append を指定すると追記モードで出力できる
  • 適切にシグナルハンドリングする(本当に適切と言えるかは分かりませんが)

以上、簡単ですが dd-time の紹介でした。

· 3 min read
Shunsuke Suzuki

作ったのは 2ヶ月くらい前の話ですが、 Go の command の timeout を実装するためのライブラリを作ったので紹介します。

https://github.com/suzuki-shunsuke/go-timeout

基本的には https://github.com/Songmu/timeout をオススメしますが、これだと上手くいかないパターンがあったので自作しました。

Go の command の timeout に関しては https://junkyard.song.mu/slides/gocon2019-spring/#24 がとても参考になります。

上記のスライドでは

  • 標準ライブラリの exec.CommandContext でも停止できるが、 SIGKILL で強制的に停止することになる
    • 子プロセスが停止しない
  • 公式見解 では、SIGKILL 以外は標準ライブラリではサポートしない。サードパーティでやればよい
  • Songmu/timeout 使えば SIGKILL 以外でより安全に停止できる

ということが丁寧に説明されています。

自分は cmdx という task runner を開発していてその中で task の実行時に timeout を設定出来るようにしました。 当初 Songmu/timeout を使って実装したのですが、問題があることに気づきました。 それは、 command の中で fzf を使うと、上手く動かないというものでした。

正直この辺の挙動はちゃんと理解できていないのですが、 調べてみると Songmu/timeout だと syscall.SysProcAttr の Setpgid を true に設定していて、そうすると fzf が上手く動かないようでした。

https://junkyard.song.mu/slides/gocon2019-spring/#48

https://junkyard.song.mu/slides/gocon2019-spring/#45

には timeout の実装方式として

  • GNU timeout の場合
  • Songmu timeout の場合

の 2 通り書いてありますが、 suzuki-shunsuke/go-timeout では GNU timeout のパターンで実装しています。

https://junkyard.song.mu/slides/gocon2019-spring/#46

に書いてあるとおり、少々乱暴な気もしますが、 cmdx で使う分には特に問題ない気がします。

· 6 min read
Shunsuke Suzuki

最近自作した OSS, cmdx の紹介です。

https://github.com/suzuki-shunsuke/cmdx

cmdx は task runner です。

task runner の定義はググってもわからなかったので、 cmdx を task runner と呼ぶのが適切かわかりませんが、 ここではプロジェクト固有のタスク

  • 依存するライブラリのインストール
  • ビルド
  • テスト
  • コード整形
  • lint
  • etc

などを管理するものとします。

類似するものとしては以下のようなものがあります。

使い方

詳細は README を読んでください。

$ cmdx -i

で設定ファイルの雛形を生成します。

そして設定ファイルに task を定義していきます。 設定に関しては README を参照してください。

そうすると cmdx -l でタスクの一覧とその説明が見れます。

例えば次は cmdx のリポジトリでの実行結果です。

$ cmdx -l
init, i - setup git hooks
coverage, c - test a package (fzf is required)
test, t - test
fmt - format the go code
vet, v - go vet
lint, l - lint the go code
release, r - release the new version
durl - check dead links (durl is required)
ci-local - run the Drone pipeline at localhost (drone-cli is required)

これにより新しくプロジェクトに参画した人もどのような task があるのか直ぐわかります。 例えば test を実行したければ cmdx t を実行すればいいことがわかります。 cmdx help test とすればここのタスクのより詳細なヘルプが見れます。

ドキュメントに task について書いても、ドキュメントがちゃんと更新されずドキュメントと実態が乖離するなんてことはよくありますが、 cmdx の設定ファイルからヘルプを生成することで乖離しにくくなります(実際に使われてない task が残ってたり、task の description や usage が間違ってたら駄目ですが)。

なぜ cmdx か

自分は今まで task runner として基本的に npm scripts を使ってきていて、ブログにも書いています。

JS以外でのnpmの活用

しかし、 npm scripts に対しては以下のような不満がありました。

  • security alert が定期的に飛んできて対応が面倒くさい
    • これは husky や commitlint などを使っているのが原因なのであって、 npm scripts の問題ではないですが
  • task に対するヘルプメッセージがない
    • 今までは README に書いてたが、本来は help コマンドで自動生成・サポートされるべきだと思っている

他のツールによってこれらの不満は解消できるのですが、他のツールにもそれぞれ微妙に不満があり、 完全に自分のニーズに合うものがなかったので作ることにしました。

一例ですが、 npm scripts は

  • 設定ファイル (package.json) を探索
  • 設定ファイルのあるディレクトリでコマンドを実行

します。これにより

  • カレントディレクトリを意識する必要がない
    • 設定ファイルのパスを指定する必要がない
    • コマンドの実行ディレクトリがカレントディレクトリに依存しない(逆に言うとカレントディレクトリに依存した処理を実行しにくいという面もありますが)

という良さがあり、 これが意外と他のツールではサポートされてなく(例えば Make だったら -F オプションで Makefile のパスを指定する必要がある)、不満でした。

また、 Make や Task では task の依存関係を定義し、一回のコマンドで複数のタスクを実行できますが、 cmdx ではそのような機能はサポートしていません。 自分が普段そのような機能をあまり必要としていないからです。

cmdx では上のような npm scripts の不満を解消するだけでなく、折角なので幾つか細かな機能を追加しています。

  • シェルスクリプトだと面倒なオプション引数をサポート
  • リッチなプロンプトのサポート
  • タイムアウト
  • etc

cmdx から npm scripts に乗り換えた場合の問題点としては husky や commitlint のようなツールが使えなくなることですが、 必須のツールでもないので許容しています。

· 4 min read
Shunsuke Suzuki

2019-07-17 追記

プロジェクト名が変わりました

https://github.com/suzuki-shunsuke/flute/issues/20


Go の HTTP client のテストフレームワークを作ったので紹介します。

https://github.com/suzuki-shunsuke/flute

執筆時点のバージョンは v0.6.0 です。

  • リクエストパラメータのテスト
  • HTTP サーバのモッキング

を目的としています。

比較的実践的なサンプルとして、ユーザーを作成する簡単な API client とそのテストを書いたので参考にしてください。

元々自分はこの目的のために h2non/gock を使っていました。 ただ、 gock だとリクエストがマッチしなかったときに、なぜマッチしないのかがわからず、調査に困るという問題がありました。

そこで flute では request に対し、matcher と tester という概念を導入し、 matcher でマッチしたリクエストを tester でテストするというふうにしました。 テストでは内部で stretchr/testify の assert を使っており、テストに失敗したときになぜ失敗したのかが分かりやすく出力されるようになっています。

例えば以下の例は、リクエストの Authorization header にトークンがセットされていなかった場合のエラーメッセージです。

=== RUN   TestClient_CreateUser
--- FAIL: TestClient_CreateUser (0.00s)
tester.go:168:
Error Trace: tester.go:168
tester.go:32
transport.go:25
client.go:250
client.go:174
client.go:641
client.go:509
create_user.go:45
create_user_test.go:56
Error: Not equal:
expected: []string{"token XXXXX"}
actual : []string{"token "}

Diff:
--- Expected
+++ Actual
@@ -1,3 +1,3 @@
([]string) (len=1) {
- (string) (len=11) "token XXXXX"
+ (string) (len=6) "token "
}
Test: TestClient_CreateUser
Messages: the request header "Authorization" should match
service: http://example.com
request name: create a user

また、当たり前かもしれませんが、モックとしてレスポンスも返します。

マッチングやテストで使える項目としては

  • リクエストパス (ex. "/users")
  • method (ex. "GET", "POST")
  • クエリパラメータ(パラメータの有無、値)
  • ヘッダー(ヘッダーの有無、値)
  • リクエストボディ
    • 文字列完全一致
    • JSONとしての等価性
  • ユーザー定義のカスタム関数

などがあります。

詳細は コード中にコメントを入れているので godoc を読んでください。

技術的には *http.Client の Transport に *flute.Transport を設定することで HTTP サーバのモッキングをしています。

API のデザイン面で考慮したこととしては、 グローバル変数である http.DefaultClient の変更をライブラリ側でやらないことです。 あくまで http.RoundTripper の実装を提供するだけで、それを http.DefaultClient に設定する場合、それのコントロールはユーザーに任せています。

  • ライブラリでグローバル変数の変更を隠蔽し、ユーザーが無意識のうちに変更してたりするのは良くない
    • gock では http.DefaultClient を変更しているが、それを理解しないまま使っているユーザーもいるはず
    • グローバル変数の変更には副作用もあるので、ユーザーが理解した上で明示的に行うべきである
    • 明示的に http.DefaultClient = client のようにユーザーに書かせれば、理解しないまま使うことはないはず
  • ライブラリの外からも変更できるグローバル変数をライブラリで完全に管理するのは不可能なので、ユーザーに任せる

以上、簡単ですが自作の OSS flute の紹介でした。

· 5 min read
Shunsuke Suzuki

Drone v0.8 の .drone.yml を v1 の .drone.jsonnet に変換するツールを作ったので紹介します。

https://github.com/suzuki-shunsuke/drone-jsonnet-generator

背景

https://docs.drone.io/user-guide/pipeline/migrating/

Drone は v0.8 から v1 で .drone.yml のフォーマットが大きく変わっています。 Drone v1 ではビルド実行時に自動で変換しているため、v0.8 の .drone.yml でもそのまま動きます(matrix builds も動きます)。

そのため、Drone v0.8 から v1 に移行する際、すぐに .drone.yml を修正しなくても問題ないのですが、 v1 独自の機能が出てきた場合 v0.8 のフォーマットの場合利用できないかもしれませんし、 いつまでも古いままだと気持ち悪いので出来るならフォーマットを変換したいです。

drone-cli ではフォーマットを変換する drone convert というコマンドが提供されています。

ただし、 drone convert は matrix build を multiple pipeline に変換するのですが、 非常に冗長になります。 そのため、jsonnet を利用することが推奨されています。

https://docs.drone.io/user-guide/pipeline/migrating/

The above syntax can be quite verbose if you are testing a large number of variations. To simplify your configuration we recommend using jsonnet.

この .drone.jsonnet の生成は drone convert では出来ないですし、手作業になるのですが、結構面倒です。 v0.8 から v1 でフォーマットが変わってますし、jsonnet に馴染みの薄い人も少なくないでしょう。 移行対象のリポジトリが多い場合、非常に苦行になります。

そこで今回この .drone.jsonnet を生成するツールを開発しました。

使い方

非常にシンプルです。

$ drone-jsonnet-generator gen [--source .drone.yml] [--target .drone.jsonnet] [--stdout]

を実行すると .drone.yml から .drone.jsonnet を生成します。

例えば

---
pipeline:
build:
image: golang:${GO_VERSION}
commands:
- echo hello
services:
database:
image: ${DATABASE}
matrix:
include:
- GO_VERSION: 1.4
DATABASE: mysql:5.5
- GO_VERSION: 1.4
DATABASE: mysql:6.5
- GO_VERSION: 1.3
DATABASE: mysql:5.5

から

local pipeline(GO_VERSION, DATABASE) = {
"kind": "pipeline",
"name": "'GO_VERSION:' + GO_VERSION + ' DATABASE:' + DATABASE",
"platform": {
"arch": "amd64",
"os": "linux"
},
"services": [
{
"image": "${DATABASE}",
"name": "database",
"pull": "default"
}
],
"steps": [
{
"commands": [
"echo hello"
],
"image": "golang:${GO_VERSION}",
"name": "build",
"pull": "default"
}
]
};

local args = [
{
"DATABASE": "mysql:5.5",
"GO_VERSION": "1.4"
},
{
"DATABASE": "mysql:6.5",
"GO_VERSION": "1.4"
},
{
"DATABASE": "mysql:5.5",
"GO_VERSION": "1.3"
}
];

[
pipeline(arg.GO_VERSION, arg.DATABASE) for arg in args
]

残念ながら生成された jsonnet はそのままでは使えません。修正が必要です。 それでも一から .drone.jsonnet を書くよりは圧倒的に効率が良いです。

  • pipeline 中の変数が ${変数名} となっているので直す ("golang:${GO_VERSION}" -> "golang:" + GO_VERSION)
  • pipeline name が " で囲まれてるので取り除く ("'GO_VERSION:' + GO_VERSION + ' DATABASE:' + DATABASE" -> 'GO_VERSION:' + GO_VERSION + ' DATABASE:' + DATABASE)

あと、一部のコードを JSON として生成しているので、jsonnet としては多少綺麗ではない(フィールドが"で囲まれてたりする)ですが、実用上特に問題ないと思います。

ちなみに上の例では matrix build の include が使われていますが、使っていない場合にも対応してますし、 include が使われていない場合、若干生成されるコードのテンプレートが違います。

なお、今回のツールの対象になる .drone.yml は matrix build を使っているのが前提になります。 matrix build が使われていないとエラーを返します。 matrix build を使っていないのであれば jsonnet を使う必要性が弱いですし、 drone convert で変換すれば良い気がします。

以上、自作のOSSの紹介でした。 Drone v0.8 から v1 への移行で困っている人は是非使ってみてください。 快適な Drone ライフを。

· 2 min read
Shunsuke Suzuki

https://github.com/suzuki-shunsuke/go-jsoneq

2つの値がJSONとして等しいか比較するGoライブラリを開発したので紹介します。

「2つの値がJSONとして等しい」とは、2つの値をそれぞれJSON文字列に変換したら、2つが表現するデータがおなじになるという意味です。

struct {
Foo string `json:"foo"`
}{
Foo: "bar",
}

map[string]interface{}{"foo": "bar"}

を JSON に変換したらともに

{"foo": "bar"}

になりますね。

json.Marshaler のテストや、 実際の JSON 文字列から構造体を定義したときにちゃんと定義できているかチェックするのに使えると思います。

jsoneq.Equal でやっていることは単純です。

  1. json.Marshal で []byte に変換
  2. json.Unmarshal で []byte を map, array と primitive な型からなるオブジェクト(?)に変換
  3. reflect.DeepEqual で比較

引数が []byte の場合は 1 は飛ばします。

GoDoc やサンプルを見れば使い方は簡単にわかると思います。

以上、簡単ですが、自作ライブラリの紹介でした。