Skip to main content

· 4 min read
Shunsuke Suzuki

Drone v1 では Extension という仕組みが導入されました。

これは文字通り Drone を拡張する仕組みで、仕様に従って作れば自由に Drone を拡張できます。

https://docs.drone.io/extensions/overview/

全てを本体でやるのではなく、拡張する仕組みを提供し、あとはコミュニティに委ねるというのが Drone の一つの方針とも言えると思います。

Extension は非常に面白い仕組みだと思いますが、 Drone を運用する立場からすると中々頭が痛い仕組みな気がしてて、 自分は導入に対し慎重な立場です。 単なる杞憂で済めば良いのですが、その懸念について書きたいと思います。

根本は Drone Extension 固有の問題と言うより、一般的な拡張機構全般に言えることだと思います。 ただし、 Drone Extension は全てのビルドに影響を及ぼす、 CI/CDシステムが動かなくなるとサービスのリリースに影響を及ぼしかねないということからよりリスクの高いものになっています。

  • 本体の drone/drone と比べ、開発は活発ではなく、サードパーティの extension はいつ開発が止まってもおかしくない
  • 本体の drone/drone と比べ、ドキュメントやサポート体制が貧弱だと思われる(drone に関しては https://discourse.drone.io でサポートされているが、サードパーティの extension では難しい)
  • ユーザーからの extension に関する要望を受け付けるようになると、管理者の負担になる
  • extension のクォリティはマチマチであり、例外処理が甘かったり、ちゃんとエラーを吐かないものもあるだろう
  • トラブルシューティングが難しいと思われる
  • extension の仕組み上、extension を必要としないビルドにも影響を及ぼしうる
  • 一度追加し、依存しだすと消すのが難しくなる
  • extension が落ちると全 build に影響するので、耐障害性(冗長化)、モニタリングが必要
  • etc

勿論、上記の懸念点は Extension によって提供される機能とトレードオフであり、 Extension の導入方針は Drone が運用される環境によって大きく依存すると思います。

例えば全員が顔見知りのような小さな組織で特定のサービス専用に Drone を使っていてかつ Drone の運用体制(人員)に十分余裕があるなら 積極的に Extension を導入しても問題ないかもしれません。

一方大きな組織で色々なサービスで同じ Drone を使っていてかつ Drone の運用体制が不十分(人手不足)ならば、 Extension の導入には慎重にならざるを得ないのではないかと思います。

· 2 min read
Shunsuke Suzuki

v0.8 では server - agent 間の通信に gPRC が使われていましたが、 v1 では使われなくなりました。

理由

v1 での通信方法


自分も v0.8 から Drone を運用していて最近 v1 に upgrade しましたが、 v0.8 では gRPC 関連のトラブルが頻発していました。 server のログでは絶えず gRPC 関連のエラーを吐いていましたし、 server - agent 間の TCP connection が切れっぱなしになって戻らくなって agent 数がどんどん減っていったり ビルドが pending のままになったり、色々ありました。

関連する issue はあり、幾つか対策を打ってみたりしましたが、結局解決しませんでした。

それが v1 にアップグレードして gRPC が使われなくなってから解消し、個人的にはとても助かりました。 管理者的にはアップグレードして一番嬉しい点ですね。

· 7 min read
Shunsuke Suzuki

今更ながら Golang での時刻の扱い方について改めて整理してみました。

まとめ

  • time.Local は明示的に設定する(基本UTC)
  • DB などには 基本UTC で永続化する
  • 出力時に必要になったらタイムゾーンを変更する
    • location は出力時に問題になるので出力時に location を明示的に指定する
    • 逆に言うと出力時以外は問題にならないので無理に location を UTC にしなくても良いかもしれない
    • サードパーティ(ex. ORM) に time.Time を渡す場合は location に注意が必要
  • 文字列として時刻の入力を受け付ける場合は location を明示的にセットする
  • サードパーティが time.Local に依存する場合、 time.Local を明示的に UTC にしたりする必要があるかもしれない
  • アプリケーションで利用する location が分かっている場合、location を取得するヘルパー関数を定義する
  • time.LoadLocation は環境依存なので予め location が分かっているなら使わないほうがよい
  • 文字列を time.Time に変換する場合、time.ParseInLocation で Location を指定して time.Time に変換後、time.Time.UTC() で UTC に変換する
  • time.Time を文字列に変換する場合、time.In で location を変換後、time.Time.Format で文字列に変換する

グローバルな location

https://golang.org/pkg/time/#Location

Local represents the system's local time zone.

location を設定する

https://crieit.net/posts/Go-time-LoadLocation に書いてあるとおり、 time.LoadLocation を下手に呼び出すと環境によっては unknown time zone エラーが起こるため 次のように time.FixedZone で Location を生成します。

jp := time.FixedZone("Asia/Tokyo", 9*60*60)

FixedZone という関数名が紛らわしい気もしますが、新しい Location を生成しているだけで副作用はありません。

ちなみに time.FixedZone に渡す文字列は "foo" みたいな適当な文字列でも動くようです。

https://golang.org/pkg/time/#FixedZone

https://github.com/golang/go/blob/9e277f7d554455e16ba3762541c53e9bfc1d8188/src/time/zoneinfo.go#L263-L308

アプリケーションで利用する location が決まっている場合、次のように location を返すヘルパー関数を用意すると良さそうです。

package location

import (
"time"
)

var (
jp *time.Location
)

func init() {
jp = time.FixedZone("Asia/Tokyo", 9*60*60)
}

func JP() *time.Location {
return jp
}

厳密に UTC な Location を取得する

厳密に言うと、time.UTC は変更可能なので UTC だとは限りません。 そのため、本来 time.UTC はゲッター関数であるべきだったんじゃないかなという気もします。

厳密に UTC な Location を取得するにはヘルパー関数を書くと良さそうです。

package location

import (
"time"
)

var (
utc *time.Location
)

func init() {
utc = time.FixedZone("UTC", 0)
}

func UTC() *time.Location {
return utc
}

動作環境に依存しないコードにするために

動作環境によって time.Local の値が違うことで結果が変わってしまう場合があります。 それを防ぐために、プログラムの最初に time.Local を UTC にするという手もありそうです。

time.Local = location.UTC()

ただし、それでもグローバル変数である以上、行儀の悪いサードパーティのライブラリによって変更されるかもしれませんし、 必要な箇所で location を明示的に指定してグローバル変数に依存しないようなコードを書くことを心がけたほうが良い気もします。

文字列を time.Time に変換する

ParseInLocation is like Parse but differs in two important ways. First, in the absence of time zone information, Parse interprets a time as UTC; ParseInLocation interprets the time as in the given location. Second, when given a zone offset or abbreviation, Parse tries to match it against the Local location; ParseInLocation uses the given location.

ParseInLocation と Parse の違い

  • 文字列に location の情報がない場合、 Parse は UTC として扱う
  • zone offset が指定された場合、 Parse は Local location からの offset として扱う
    • ParseInLocation で明示的に Location を指定したほうが良さそう

予め location がわかっている場合 time.ParseInLocation で location を指定して time.Time に変換した後 time.Time.In で UTC にするのが良さそうです。

t, err := time.ParseInLocation("2006-01-02T15:04:05", "2019-08-13T21:30:00", jp)
if err != nil {
return err
}
t = t.UTC()

time.Time の Location を変更する

https://golang.org/pkg/time/#Time.In

In returns a copy of t representing the same time instant, but with the copy's location information set to loc for display purposes.

time.Time.In は time.Time の Location だけ変更したコピーを返します。

package main

import (
"fmt"
"time"
)

func main() {
time.Local = location.UTC()
t := time.Now()
fmt.Println(t) // 2019-08-14 12:08:44.150725 +0000 UTC m=+0.000212031
t2 := t.In(location.JP())
fmt.Println(t2) // 2019-08-14 21:08:44.150725 +0900 Asia/Tokyo
}

time.Time を文字列に変換する

https://golang.org/pkg/time/#Time.Format

time.Time.In で location を変更した後 time.Time.Format で文字列に変換するのが良さそうです。

time.Now の location

Location は time.Local になります。

https://golang.org/pkg/time/#Now

他のパッケージの location の扱い

log

log パッケージで出力される時刻のフォーマットと location は log.SetFlags によってある程度変更できます。

デフォルトは 日時を time.Local で出力します。 log.LUTC をセットすることで UTC になります。

log.SetFlags(log.Flags() | log.LUTC)

logrus

logrus のログの時刻の location も time.Local なようです。

robfig/cron

https://github.com/robfig/cron

All interpretation and scheduling is done in the machine's local time zone (as provided by the Go time package (http://www.golang.org/pkg/time).

time.Local なようです。

go-sql-driver/misql

gorm

https://github.com/jinzhu/gorm/wiki/How-To-Do-Time

· 2 min read
Shunsuke Suzuki

Drone では v1 から冗長な YAML を DRY にする一つの手として、 Jsonnet の利用が推奨されています。 これについては過去のブログでも触れています。

https://techblog.szksh.cloud/drone-jsonnet-generator/

しかし、 v1 の rc の時点では Jsonnet の活用には Jsonnet Extension が必要でした。

https://engineering.linecorp.com/ja/blog/go-oss-ci-cd-platform-drone-1-0-0-rc-1/#title7-1

しかし、 v1 の正式版では Jsonnet Extension がなくても Jsonnet が利用できるようになっています。

まず Drone の管理者側で Drone server に環境変数 DRONE_JSONNET_ENABLED=true を設定する必要があります。

そうしたら、ユーザー側は次のようにすることで jsonnet が使えます。

  1. .drone.yml の代わりに .drone.jsonnet をコミットする (.drone.yml は不要)
  2. 各リポジトリの settings の Main > Configuration で設定ファイルのパスを変更する

こうすることでビルド実行時に自動で Jsonnet が YAML に変換され処理されるようです。

いつから Jsonnet Extension は不要になったのか

v1 の rc ではサポートされてませんでしたが、正式版をリリースするタイミングで Jsonnet Extension が不要になっていたようです。

注意点

  • SaaS https://cloud.drone.io は 2019-08-02 時点で未対応
  • Jsonnet は pipeline が 1 つでも [] で囲い、配列にする必要がある
  • Jsonnet の Imports はサポートしていない

リファレンス

· 6 min read
Shunsuke Suzuki

The Top 10 Most Common Mistakes I’ve Seen in Go Projects という記事を読んで面白かったのでメモります。 翻訳ではないです。メモなので、原文を読んでください。

  1. Unknown Enum Value: Unknown であることを表す enum の値は 0 にしよう。値がセットされていない場合に Unknown として扱えるから
  2. Benchmarking: ベンチマークを取るのは難しい。コンパイラの最適化によってベンチマークの結果が不適切になる場合がある
  3. Pointers! Pointers Everywhere!: パフォーマンスの観点から基本的にはポインタを使うべきではない。変数を共有する必要がある場合のみ、ポインタを使う
  4. Breaking a for/switch or a for/select: for, switch が入れ子になっている場合、switch の中で break しても for から抜けられない。抜けたければ labeled break を使う
  5. Errors Management
  6. Slice Initialization
  7. Context Management
  8. Not Using the -race Option: go test コマンドでは -race オプションをつけよう
  9. Using a Filename as an Input: 引数としてファイル名を渡すのではなく、 io.Reader や io.Writer を渡そう
  10. Goroutines and Loop Variables

Pointers! Pointers Everywhere!

変数はヒープかスタックに割り当てられる。

  • スタック: 関数内の変数は、関数が返されると、スタックからポップされる
  • ヒープ: 共有変数、グローバル変数

関数で生成した構造体を返すと、スタックで管理され、返された時点でポップされる。 関数内で生成したポインタを返すと、それは Heap で管理される。 スタックで管理すると、関数が返された時点でポップされてしまい、関数の外でポインタが指す値にアクセスできなくなるため。

func getFooValue() foo {
var result foo
// Do something
return result
}
func getFooPointer() *foo {
var result foo
// Do something
return &result
}

スタックのほうが効率が良い理由

  • ガベージコレクタが不要
    • 関数を抜けた時点でスタックからポップされる
    • 未使用の変数を回収する複雑な処理が不要
  • スタックの変数は一つの goroutine に属するため、共有のための同期が不要だから

よって基本的にはポインタを使うべきではない。変数を共有する必要がある場合のみ、ポインタを使う。

Error Management

  • エラーは一回だけハンドリングされるべき。エラーはロギングされるか、プロパゲートされるべき(ロギングしつつプロパゲートはだめ)
  • pkg/errors を使うと根本的なエラーの型を見て条件分岐できる
    • 自分が作ってる go-errlog の v0.9.1 だとそれは出来ない。改善すべきか
switch errors.Cause(err).(type) {
default:
log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
return Status{ok: false}
case *db.DBError:
return retry(customer)
}

Slice Initialization

https://tour.golang.org/moretypes/11

  • slice には length と capacity がある
  • length は slice が保持する要素の数
  • capacity は slice の裏にある配列の要素数を slice の最初の要素から数えたもの

https://golang.org/ref/spec#Making_slices_maps_and_channels

slice の場合

make(T, n) // length, capacity 共に n
make(T, n, m) // length は n, capacity は m

https://play.golang.org/p/R1CF1e1K3L6

a := make([]int, 0, 10)
fmt.Println(len(a), cap(a)) // 0, 10
a = append(a, 1)
fmt.Println(len(a), cap(a)) // 1, 10

append はコストが高いからインデックスを指定したほうが良いというが、 make でスライスを生成していれば、append してもそこまでコストは高くない。 若干インデックスを指定したほうが効率が良いが、一貫性という観点では append を使ったほうがよいかもしれない。

Goroutines and Loop Variables

ints := []int{1, 2, 3}
for _, i := range ints {
go func() {
fmt.Printf("%v\n", i)
}()
}

これだと全ての goroutine で同じ i を共有してしまう。関数の引数として渡すか、for ループ内の変数として定義する。

ints := []int{1, 2, 3}
for _, i := range ints {
go func(i int) {
fmt.Printf("%v\n", i)
}(i)
}
ints := []int{1, 2, 3}
for _, i := range ints {
i := i // 初見だと奇妙に見えるが、正しい。
go func() {
fmt.Printf("%v\n", i)
}()
}

· 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 の紹介でした。

· 3 min read
Shunsuke Suzuki

Jenkins では parameterized build という機能で、ビルド実行時に Web UI からパラメータを指定することができます。

Drone では基本的に Git のイベントをフックして動くので「ビルドを実行時に手動でパラメータを設定する」ということは出来ません。

自分は基本的にできなくても構わないと思っていますが、 こういった機能がないから Drone を使わないという人も中にはいるので、 Drone でもちょっとした工夫でそれっぽいことは出来るんじゃないかと思い、簡単なサンプルを書いてみました。

一応言っておくと、 Jenkins の parameterized build を完全に代替するようなものではありません。

https://github.com/suzuki-shunsuke/example-drone-build-parameter

以下のファイルが必要です。

スクリプトを実行してデプロイします。

$ bash scripts/deploy.sh

するとパラメータを記述するファイルがテンプレートから作成され、エディタで開きます。

https://github.com/suzuki-shunsuke/example-drone-build-parameter/blob/master/scripts/deploy.sh#L12-L17

パラメータを記述し、エディタを閉じます。

するとそのファイルがコミットされ、新しいタグが作成され、コミットとタグがリモートにプッシュされます。

https://github.com/suzuki-shunsuke/example-drone-build-parameter/blob/master/scripts/deploy.sh#L27-L35

Drone でタグをプッシュするイベントをフックしてビルドが実行されます。

https://github.com/suzuki-shunsuke/example-drone-build-parameter/blob/master/.drone.yml#L13-L17

ビルドではコミットされたパラメータの設定ファイルを読み込むことでビルドにパラメータを渡せます。

https://github.com/suzuki-shunsuke/example-drone-build-parameter/blob/master/.drone.yml#L10

こうすることでビルドにパラメータを渡すことができます。 パラメータの設定ファイルはコミットされるので Git で管理できるというのも特徴です。

https://github.com/suzuki-shunsuke/example-drone-build-parameter/blob/master/build_params/2019-07-07T10-04-02JST/params.sh

上記のスクリプトではパラメータの設定ファイルとしてシェルスクリプトで環境変数を定義していますが、 シェルスクリプトである必要性はなく、例えば JSON ファイルを記述してビルドで JSON ファイルを読み込んでもよいし、 パラメータを選択させるようなことがしたければ fzf のようなものを使ってもよいし、 いくらでも改善できます。

以上、簡単な tips でした。

· 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 ライフを。

· 3 min read
Shunsuke Suzuki

久しぶりに Drone plugin を作ったので紹介します。

https://www.github.com/suzuki-shunsuke/drone-plugin-jsonnet-check

.drone.jsonnet から .drone.yml を生成していて、両方を Git で管理している場合に、 .drone.jsonnet と .drone.yml の状態が一致しているかテストするための plugin です。

Drone v1 では matrix builds が廃止され、multiple pipeline が導入されました。 matrix builds を drone convert コマンドで multiple pipeline に変換すると、pipeline の数が多いほど冗長でメンテナンス性が悪くなります。 そこで公式では jsonnet で記述して .drone.yml に変換する方法が推奨されています。

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

To simplify your configuration we recommend using jsonnet.

$ drone jsonnet --format --stream

jsonnet から yaml への変換は Jsonnet extension を使うと Drone がビルド実行時に自動で変換してくれるので .drone.yml を管理する必要はなくなりますが、 使っていない場合、 .drone.jsonnet と .drone.yml を Git で管理し、自前で変換してコミットする必要があります。 この作業をなにかしら自動化しないと .drone.jsonnet と .drone.yml に不整合が生じることもあり得ると思います。

  • .drone.jsonnet を更新したけど .drone.yml を更新し忘れる
  • .drone.yml を直接更新してしまった

そこで CI で不整合が生じていないかテストするための plugin を作りました。

最初は plugin ではなく、ただのサンプルコードとして公開しました。

https://github.com/suzuki-shunsuke/drone-jsonnet-convert-test

しかし、折角なので plugin にしました。

やっていることは単純で、 .drone.jsonnet から YAML を生成し、 .drone.yml と diff を取っているだけです。

使い方は README を見れば分かると思いますが、

drone jsonnet コマンドのオプションを plugin のパラメータとして渡せます。

{
kind: "pipeline",
name: "test",
steps: [
{
name: "test .drone.yml",
image: "suzukishunsuke/jsonnet-check:v1.1.1-v0.1.0",
settings: {
format: true,
},
},
],
}
$ drone exec
[test .drone.yml:0] + drone jsonnet --format --target /tmp/.drone.yml
[test .drone.yml:1] + diff .drone.yml /tmp/.drone.yml
[test .drone.yml:2] --- .drone.yml
[test .drone.yml:3] +++ /tmp/.drone.yml
[test .drone.yml:4] @@ -12,5 +12,4 @@
[test .drone.yml:5] settings:
[test .drone.yml:6] format: true
[test .drone.yml:7]
[test .drone.yml:8] -
[test .drone.yml:9] ...
2019/06/01 00:33:27 test .drone.yml : exit code 1

シェルスクリプトで実装しました。 テストでは bats を使っています。

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

· 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 やサンプルを見れば使い方は簡単にわかると思います。

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