goa で multipart/form-data を扱う
multipart/form-data
という MIME タイプがあります。これは主に HTML フォームの内容をサーバに対して POST する際に用いられます。
goa の Request Context には http.Request が埋まっているので、そこから MultipartReader() や ParseMultipartForm() を呼び出すことで multipart/form-data
を扱うことができます。以下は goa の公式サンプルに収録されているコードです。
func (c *ImageController) Upload(ctx *app.UploadImageContext) error { // Assumes the image is under multipart section named "file" reader, err := ctx.MultipartReader() if err != nil { return goa.ErrBadRequest("failed to load multipart request: %s", err) } if reader == nil { return goa.ErrBadRequest("not a multipart request") } var images []*app.ImageMedia for { p, err := reader.NextPart() if err == io.EOF { break } if err != nil { return goa.ErrBadRequest("failed to load part: %s", err) } f, err := os.OpenFile("./images/"+p.FileName(), os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return fmt.Errorf("failed to save file: %s", err) // causes a 500 response } defer f.Close() io.Copy(f, p) data := c.saveImage(p.FileName()) images = append(images, &app.ImageMedia{ID: data.ID, Filename: data.Filename, UploadedAt: data.UploadedAt}) } return ctx.OK(images) }
multipart.Reader を介して、各 part の内容を取り出しています。
このように、従来から goa では multipart/form-data
を扱うことができたのですが、これはあくまでサーバ実装の話です。 design (設計) の中で multipart/form-data
であることを明示するための DSL は用意されていなかったので、 Swagger にも formData
として定義を出力することはできませんでした。
しかし今回、この機能に関する MultipartForm という新しい DSL が追加になりました。今後は design レベルで multipart/form-data
を取り扱うことができます。
MultipartForm
の使用例について、まずは design から見てみましょう。
var _ = Resource("profiles", func() { Action("submit", func() { Routing(POST("profiles")) Payload(ProfilePayload) MultipartForm() Description("Post accepts a multipart form encoded request") Response(OK, ResultMedia) }) }) var ProfilePayload = Type("ProfilePayload", func() { Attribute("name", String, "Name") Attribute("birthday", DateTime, "Birthday") Required("name", "birthday") })
MultipartForm
は Action DSL の中で使用します。これによって goa は当該 Action の Payload を multipart/form-data
として扱うようになります。
これを元に出力される Swagger 定義は以下のようになります。
/profiles: post: description: Post accepts a multipart form encoded request operationId: profiles#submit parameters: - description: Birthday in: formData name: birthday required: true type: string - description: Name in: formData name: name required: true type: string produces: - application/vnd.goa.example.form responses: "200": description: OK schema: $ref: '#/definitions/ResultMedia' schemes: - http summary: submit profiles tags: - profiles
parameters
に書かれている各パラメータが in: formData
になっています。これは OpenAPI Specification Version 2 で規定されている form-data を表す定義になります。
最後に controllers の実装です。冒頭に記述した通り、従来は multipart をパースして各 part をひとつずつ取り出す必要がありました。今回の機能追加後、コードは以下の形になります。
func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error { res := &app.ResultMedia{ Name: ctx.Payload.Name, Birthday: ctx.Payload.Birthday, } return ctx.OK(res) }
これだけです。 multipart のパースは、このコードに到達する前に goa が自動で行います。各 part の内容は既に ctx.Payload
のフィールドにセットされているので、それを使ったコードを書くだけで良いのです。
より詳細な例は以下をご覧ください。
2017 年振り返り
OSS
昨年に引き続き goa にコミットしていました。 2018 年は v2 が形になるといいですね。
勉強会
goa 勉強会で発表もしました。
転職
2 年半勤めた前職を辞めて 11 月から Pacific Porter で働いています。引き続き仕事でも Go を (goa も) 書く毎日です。前職では新規開発に関わることが多かったので、既に動いているシステムに関わるのは久しぶりです。だんだん仕組みがわかっていくのが楽しいですね。
総括
10 月あたりから解けるように時間が過ぎていました。どう考えてもスプラトゥーンのせいです。本当にありがとうございました。
Splatoon 2 (スプラトゥーン2) 【オリジナルマリオグッズが抽選で当たるシリアルコード配信(2017/10/26-2018/1/8注文分まで)】 - Switch
- 出版社/メーカー: 任天堂
- 発売日: 2017/07/21
- メディア: Video Game
- この商品を含むブログ (8件) を見る
Go で書かれた API Gateway "KrakenD"
これは Go3 Advent Calendar 2017 の 10 日目の記事です。
小さな役割を持つサービス群でアプリケーションを構成する、いわゆる Microservices が流行して久しいですね。サービスが細かく分割されるとき、それらを取りまとめるサービスが必要になることがあります。一般に API Gateway や API Aggregator と呼ばれるものです。
そんな API Gateway のひとつに Go で書かれた KrakenD というものがあります。
KrakenD - Open source API Gateway
今回はこれを試してみます。
仕様
複数のバックエンド API を集約 (aggregate) する動きを見るため、以下のような仕様でサービスを構築してみます。
- バックエンドに GitHub と Qiita の Web API を使用する
- KrakenD に
/users/:id
というエンドポイントを定義する - そこにリクエストが来たらバックエンドの GitHub と Qiita の API にリクエストする
- GitHub は
GET https://api.github.com/users/:id
- Qiita は
GET https://qiita.com/api/v2/users/:id
- GitHub は
- それぞれからのレスポンスを
"github"
、"qiita"
というフィールドに入れてクライアントにレスポンスする
シーケンス図にすると以下のような感じですね。
client krakend github qiita | | | | +--------->| | | GET /users/:id | request | | | | | | | | +--------->| | GET https://api.github.com/users/:id | | request | | | | | | | +----------^--------->| GET https://qiita.com/api/v2/users/:id | | | request | | | | | | |<---------+ | | | response | | | | | | | |<---------^----------+ | | | response | | | | | |<---------+ | | | response | | | | | | |
KrakenD の設定ファイル
まず上記の仕様を KrakenD の設定ファイル (JSON) として用意する必要があります。なんと設定ファイルを作るための Web エディタが用意されているのでそれを使わせてもらいましょう。
KrakenDesigner - KrakenD API Gateway
見た目はこんな感じ。
UI もなかなか悪くなく、サクッと設定ファイルを作ることができました。
{ "version": 1, "default_max_rate": 0, "client_max_rate": 0, "throttling_header": "", "timeout": "3000ms", "cache_ttl": "300s", "name": "My service", "host": [ "https://api.github.com", "https://qiita.com" ], "endpoints": [ { "endpoint": "/users/{id}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/api/v2/users/{id}", "encoding": "json", "host": [ "https://qiita.com" ], "group": "qiita" }, { "url_pattern": "/users/{id}", "encoding": "json", "host": [ "https://api.github.com" ], "group": "github" } ] } ] }
KrakenD サービスの立ち上げ
最近のソフトウェアらしく Docker Hub でイメージが提供されているので、それを試してみましょう。
$ docker pull devopsfaith/krakend $ docker run -p 8080:8080 -v $PWD:/etc/krakend/ devopsfaith/krakend
簡単ですね。ではリクエストを送ってみます。 HTTP クライアントは HTTPie です。
$ http localhost:8000/users/tchssk { "github": { "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4", "bio": null, "blog": "https://tchssk.github.io", "company": null, "created_at": "2015-05-05T15:43:31Z", "email": null, "events_url": "https://api.github.com/users/tchssk/events{/privacy}", "followers": 6, "followers_url": "https://api.github.com/users/tchssk/followers", "following": 3, "following_url": "https://api.github.com/users/tchssk/following{/other_user}", "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}", "gravatar_id": "", "hireable": null, "html_url": "https://github.com/tchssk", "id": 12257128, "location": "Tokyo, Japan", "login": "tchssk", "name": "Taichi Sasaki", "organizations_url": "https://api.github.com/users/tchssk/orgs", "public_gists": 0, "public_repos": 33, "received_events_url": "https://api.github.com/users/tchssk/received_events", "repos_url": "https://api.github.com/users/tchssk/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions", "type": "User", "updated_at": "2017-12-05T10:08:00Z", "url": "https://api.github.com/users/tchssk" }, "qiita": { "description": "A web developer who loves Ginger Ale !!", "facebook_id": "", "followees_count": 0, "followers_count": 2, "github_login_name": "tchssk", "id": "tchssk", "items_count": 11, "linkedin_id": "", "location": "Tokyo, Japan", "name": "Taichi Sasaki", "organization": "", "permanent_id": 83093, "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/83093/profile-images/1473702659", "twitter_screen_name": "tchssk", "website_url": "https://tchssk.github.io" } }
ちゃんと "github"
と "qiita"
にそれぞれのレスポンスが入った形で返ってきました。
レスポンスの操作
KrakenD は、バックエンドから受け取った情報をクライアントにレスポンスする前に以下の方法で操作することができます。
- Merging
- Filtering
- Grouping
- Mapping
- Capturing
では順番に試してみましょう。
Merging
複数のバックエンドからのレスポンスをマージすることができます。
"endpoints": [ { "endpoint": "/users/{id}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/api/v2/users/{id}", "encoding": "json", "host": [ "https://qiita.com" ] }, { "url_pattern": "/users/{id}", "encoding": "json", "host": [ "https://api.github.com" ] } ] } ]
上記のように設定すると GitHub と Qiita からのレスポンスがマージされます。
{ "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4", "bio": null, "blog": "https://tchssk.github.io", "company": null, "created_at": "2015-05-05T15:43:31Z", "description": "A web developer who loves Ginger Ale !!", "email": null, "events_url": "https://api.github.com/users/tchssk/events{/privacy}", "facebook_id": "", "followees_count": 0, "followers": 6, "followers_count": 2, "followers_url": "https://api.github.com/users/tchssk/followers", "following": 3, "following_url": "https://api.github.com/users/tchssk/following{/other_user}", "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}", "github_login_name": "tchssk", "gravatar_id": "", "hireable": null, "html_url": "https://github.com/tchssk", "id": 12257128, "items_count": 11, "linkedin_id": "", "location": "Tokyo, Japan", "login": "tchssk", "name": "Taichi Sasaki", "organization": "", "organizations_url": "https://api.github.com/users/tchssk/orgs", "permanent_id": 83093, "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/83093/profile-images/1473702659", "public_gists": 0, "public_repos": 33, "received_events_url": "https://api.github.com/users/tchssk/received_events", "repos_url": "https://api.github.com/users/tchssk/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions", "twitter_screen_name": "tchssk", "type": "User", "updated_at": "2017-12-05T10:08:00Z", "url": "https://api.github.com/users/tchssk", "website_url": "https://tchssk.github.io" }
フィールド名が重複している場合はどちらか片方のレスポンスが出力されるようです。
Filtering
2 種類のフィルタリングが利用できます。
Blacklisting
blacklist に指定されたフィールドを除外することができます。
"endpoints": [ { "endpoint": "/users/{id}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/users/{id}", "encoding": "json", "host": [ "https://api.github.com" ], "blacklist": [ "avatar_url", "bio", "blog", "company" ] } ] } ]
上記のように設定すると avatar_url
、 bio
、 blog
、 company
が除外されます。
{ "created_at": "2015-05-05T15:43:31Z", "email": null, "events_url": "https://api.github.com/users/tchssk/events{/privacy}", "followers": 6, "followers_url": "https://api.github.com/users/tchssk/followers", "following": 3, "following_url": "https://api.github.com/users/tchssk/following{/other_user}", "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}", "gravatar_id": "", "hireable": null, "html_url": "https://github.com/tchssk", "id": 12257128, "location": "Tokyo, Japan", "login": "tchssk", "name": "Taichi Sasaki", "organizations_url": "https://api.github.com/users/tchssk/orgs", "public_gists": 0, "public_repos": 33, "received_events_url": "https://api.github.com/users/tchssk/received_events", "repos_url": "https://api.github.com/users/tchssk/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions", "type": "User", "updated_at": "2017-12-05T10:08:00Z", "url": "https://api.github.com/users/tchssk" }
Whitelisting
whitelist に指定されたフィールドだけを出力することができます。
"endpoints": [ { "endpoint": "/users/{id}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/users/{id}", "encoding": "json", "host": [ "https://api.github.com" ], "whitelist": [ "subscriptions_url", "type", "updated_at", "url" ] } ] } ]
上記のように設定すると subscriptions_url
、 type
、 updated_at
、 url
だけが出力されます。
{ "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions", "type": "User", "updated_at": "2017-12-05T10:08:00Z", "url": "https://api.github.com/users/tchssk" }
Grouping
グルーピングを行うことができます。冒頭で扱った GitHub と Qiita の例が正にこれですね。
Mapping
バックエンドからのレスポンスを別名のフィールドにマッピングすることができます。
"endpoints": [ { "endpoint": "/users/{id}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/users/{id}", "encoding": "json", "host": [ "https://api.github.com" ], "mapping": { "blog": "personal_blog" } } ] } ]
上記のように設定すると blog
が personal_blog
にマッピングされます。
{ "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4", "bio": null, "company": null, "created_at": "2015-05-05T15:43:31Z", "email": null, "events_url": "https://api.github.com/users/tchssk/events{/privacy}", "followers": 6, "followers_url": "https://api.github.com/users/tchssk/followers", "following": 3, "following_url": "https://api.github.com/users/tchssk/following{/other_user}", "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}", "gravatar_id": "", "hireable": null, "html_url": "https://github.com/tchssk", "id": 12257128, "location": "Tokyo, Japan", "login": "tchssk", "name": "Taichi Sasaki", "organizations_url": "https://api.github.com/users/tchssk/orgs", "personal_blog": "https://tchssk.github.io", "public_gists": 0, "public_repos": 33, "received_events_url": "https://api.github.com/users/tchssk/received_events", "repos_url": "https://api.github.com/users/tchssk/repos", "site_admin": false, "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions", "type": "User", "updated_at": "2017-12-05T10:08:00Z", "url": "https://api.github.com/users/tchssk" }
Caputuring
特定フィールドのオブジェクトだけを抽出することができます。ここでは GitHub API のリポジトリを取得するエンドポイントで試してみます。
$ http https://api.github.com/repos/golang/go { "id": 23096959, "name": "go", "full_name": "golang/go", "owner": { "login": "golang", "id": 4314092, "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4", "gravatar_id": "", "url": "https://api.github.com/users/golang", "html_url": "https://github.com/golang", "followers_url": "https://api.github.com/users/golang/followers", "following_url": "https://api.github.com/users/golang/following{/other_user}", "gists_url": "https://api.github.com/users/golang/gists{/gist_id}", "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/golang/subscriptions", "organizations_url": "https://api.github.com/users/golang/orgs", "repos_url": "https://api.github.com/users/golang/repos", "events_url": "https://api.github.com/users/golang/events{/privacy}", "received_events_url": "https://api.github.com/users/golang/received_events", "type": "Organization", "site_admin": false }, "private": false, "html_url": "https://github.com/golang/go", "description": "The Go programming language", "fork": false, "url": "https://api.github.com/repos/golang/go", "forks_url": "https://api.github.com/repos/golang/go/forks", "keys_url": "https://api.github.com/repos/golang/go/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/golang/go/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/golang/go/teams", "hooks_url": "https://api.github.com/repos/golang/go/hooks", "issue_events_url": "https://api.github.com/repos/golang/go/issues/events{/number}", "events_url": "https://api.github.com/repos/golang/go/events", "assignees_url": "https://api.github.com/repos/golang/go/assignees{/user}", "branches_url": "https://api.github.com/repos/golang/go/branches{/branch}", "tags_url": "https://api.github.com/repos/golang/go/tags", "blobs_url": "https://api.github.com/repos/golang/go/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/golang/go/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/golang/go/git/refs{/sha}", "trees_url": "https://api.github.com/repos/golang/go/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/golang/go/statuses/{sha}", "languages_url": "https://api.github.com/repos/golang/go/languages", "stargazers_url": "https://api.github.com/repos/golang/go/stargazers", "contributors_url": "https://api.github.com/repos/golang/go/contributors", "subscribers_url": "https://api.github.com/repos/golang/go/subscribers", "subscription_url": "https://api.github.com/repos/golang/go/subscription", "commits_url": "https://api.github.com/repos/golang/go/commits{/sha}", "git_commits_url": "https://api.github.com/repos/golang/go/git/commits{/sha}", "comments_url": "https://api.github.com/repos/golang/go/comments{/number}", "issue_comment_url": "https://api.github.com/repos/golang/go/issues/comments{/number}", "contents_url": "https://api.github.com/repos/golang/go/contents/{+path}", "compare_url": "https://api.github.com/repos/golang/go/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/golang/go/merges", "archive_url": "https://api.github.com/repos/golang/go/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/golang/go/downloads", "issues_url": "https://api.github.com/repos/golang/go/issues{/number}", "pulls_url": "https://api.github.com/repos/golang/go/pulls{/number}", "milestones_url": "https://api.github.com/repos/golang/go/milestones{/number}", "notifications_url": "https://api.github.com/repos/golang/go/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/golang/go/labels{/name}", "releases_url": "https://api.github.com/repos/golang/go/releases{/id}", "deployments_url": "https://api.github.com/repos/golang/go/deployments", "created_at": "2014-08-19T04:33:40Z", "updated_at": "2017-12-07T12:12:01Z", "pushed_at": "2017-12-07T05:10:23Z", "git_url": "git://github.com/golang/go.git", "ssh_url": "git@github.com:golang/go.git", "clone_url": "https://github.com/golang/go.git", "svn_url": "https://github.com/golang/go", "homepage": "https://golang.org", "size": 166440, "stargazers_count": 35170, "watchers_count": 35170, "language": "Go", "has_issues": true, "has_projects": false, "has_downloads": true, "has_wiki": true, "has_pages": false, "forks_count": 4781, "mirror_url": null, "archived": false, "open_issues_count": 3158, "license": { "key": "bsd-3-clause", "name": "BSD 3-clause \"New\" or \"Revised\" License", "spdx_id": "BSD-3-Clause", "url": "https://api.github.com/licenses/bsd-3-clause" }, "forks": 4781, "open_issues": 3158, "watchers": 35170, "default_branch": "master", "organization": { "login": "golang", "id": 4314092, "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4", "gravatar_id": "", "url": "https://api.github.com/users/golang", "html_url": "https://github.com/golang", "followers_url": "https://api.github.com/users/golang/followers", "following_url": "https://api.github.com/users/golang/following{/other_user}", "gists_url": "https://api.github.com/users/golang/gists{/gist_id}", "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/golang/subscriptions", "organizations_url": "https://api.github.com/users/golang/orgs", "repos_url": "https://api.github.com/users/golang/repos", "events_url": "https://api.github.com/users/golang/events{/privacy}", "received_events_url": "https://api.github.com/users/golang/received_events", "type": "Organization", "site_admin": false }, "network_count": 4781, "subscribers_count": 2583 }
Go のリポジトリですね。これを KrakenD を通して取得します。
"endpoints": [ { "endpoint": "/repos/{owner}/{repo}", "method": "GET", "concurrent_calls": 1, "client_max_rate": 0, "querystring_params": [], "backend": [ { "url_pattern": "/repos/{owner}/{repo}", "encoding": "json", "host": [ "https://api.github.com" ], "target": "owner" } ] } ]
上記のように設定すると owner
のオブジェクトだけが抽出されます。
{ "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4", "events_url": "https://api.github.com/users/golang/events{/privacy}", "followers_url": "https://api.github.com/users/golang/followers", "following_url": "https://api.github.com/users/golang/following{/other_user}", "gists_url": "https://api.github.com/users/golang/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/golang", "id": 4314092, "login": "golang", "organizations_url": "https://api.github.com/users/golang/orgs", "received_events_url": "https://api.github.com/users/golang/received_events", "repos_url": "https://api.github.com/users/golang/repos", "site_admin": false, "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/golang/subscriptions", "type": "Organization", "url": "https://api.github.com/users/golang" }
その他の機能
今回は試しませんでしたが、一般的な API Gateway にあるような以下の機能も備えているようです。
- 複数のバックエンドへリクエストを分散する Load balancing
- 一定時間あたりのリクエスト数を制限する Rate limits
- 障害時などにバックエンドへのリクエストを遮断する Circuit breaker
性能
ベンチマークによると秒間処理リクエスト数は 3500 程度で、 OSS の API Gateway として有名な Kong の約 2 倍だそうです。
利用プラン
公式ページに書かれている以下の 3 種類のプランがあるようです。
- Open Source
- Free
- Enterprise
KrakenD のコア機能は OSS として GitHub で公開されていて、好きな Web framework と組み合わせて使えるようになっています。それがおそらく Open Source プラン。 DockerHub のイメージや RPM などを介してビルド済みのものを利用するのが Free プランでしょう。 Enterprise には mailto:
のボタンが置かれているだけなので、まだ本格的なマネタイズは行なっていないのかもしれません。
今回は少し触ってみた程度ですが、 API Gateway が必要になったときはもう少し詳しく検証して利用を検討してみたいと思いました。
goa で静的ファイル配信に go-bindata を使う
goa には静的ファイル配信のための DSL として Files が用意されており、以下のような定義を行うと FileServer を生成してくれます。
Files("/swagger/*filepath", "public/swagger/")
生成される FileServer
は http.FileSystem という interface を通してファイルオープンを行うのですが、従来は http.Dir という実装を固定で使う仕様になっていました。 http.Dir
は実行環境のネイティブファイルシステムからファイルオープンを行います。これは、ビルドしたバイナリを実行環境にデプロイする際に Files
で定義した静的ファイルもそのファイルシステム上に配置する必要がある、ということを意味します。
Go は、ビルドしたバイナリひとつをデプロイすれば動作する、いわゆるシングルバイナリが魅力のひとつですが、 goa の FileServer
でそれを実現するためには http.Dir
以外の http.FileSystem
実装を使えた方が都合がいいです。という訳で変更できるようにしました。
使い方は簡単です。例として go-bindata を使って実装してみましょう。あらかじめ go-bindata
で目的の静的ファイルを Go のソースコード化しておき、該当の controller をマウントする前にそのコードを使用するよう FileSystem
というフィールドを差し替えます。
c1 := controllers.NewSwaggerController(service) c1.FileSystem = func(dir string) http.FileSystem { return &assetfs.AssetFS{ Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: dir, } } app.MountSwaggerController(service, c1)
go-bindata
で生成されたコードを http.FileSystem
にラップするライブラリとして go-bindata-assetfs を使用しています。これで該当の静的ファイルは Go の内部でオープンされるようになるのでデプロイする必要がなくなります。ちなみに、記事のタイトルと例では go-bindata
を取り上げましたが、 FileSystem
フィールドの関数は http.FileSystem
を返せば良いので他のライブラリを使うこともできます。
より詳細な例は以下をご覧ください。
goa へのプルリクエストの送り方 (バックポート編)
goa へのプルリクエストの送り方 (バックポート編) です。
goa では互換性維持のため master と v1 を別ブランチで管理しています。そのため、プルリクエストが master にマージされたあと、その変更を v1 ブランチにバックポートするようお願いされることがあります。本編ではその手順について解説します。
1. リポジトリに移動
コンソールで以下のコマンドを実行します。
$ cd $GOPATH/src/github.com/goadesign/goa
2. v1 から新しいブランチをチェックアウト
コンソールで以下のコマンドを実行します。
$ git fetch $ git checkout -b $BRANCHNAME origin/v1
$BRANCHNAME
には任意のブランチ名を入れてください。例えばブランチ名を foo-bar-v1 とする場合は以下の様になります。
$ git checkout -b foo-bar-v1 origin/v1
3. master にマージされたコミットのハッシュを取得
コンソールで以下のコマンドを実行し、 master にマージされたブランチから対象コミットのハッシュを取得します。
$ git log --oneline $TOPICBRANCHNAME
$TOPICBRANCHNAME
にはプルリクエスト編の 2. で指定したブランチ名を入れてください。今回の例ではブランチ名を foo-bar としていたため以下の様になります。
$ git log --oneline foo-bar
コマンドを実行すると以下のようにコミットログが出力されます。
a5e8203 Use os.Getwd instead of filepath.Abs 2e47e58 Delete a debug print f54c587 Smart package name for gen_controller
先頭カラム (a5e8203
の部分) がコミットハッシュなのでこれを控えておきます。
4. master にマージされたコミットをチェリーピック
コンソールで以下のコマンドを実行し、 3. で取得したコミットをチェリーピックします。
$ git cherry-pick $COMMITHASH
$COMMITHASH
には 3. で取得したコミットハッシュを入れてください。コミットが複数ある場合はチェリーピックもその回数行います。
5. make できることを確認
コンソールで以下のコマンドを実行します。
$ make
これにはプログラムのビルドやテストコードの実行が含まれています。何らかのエラーが発生した場合はコードを修正しそれらを解決します。
6. リモートリポジトリにプッシュ
コンソールで以下のコマンドを実行します。
$ git push -u mine $BRANCHNAME
$BRANCHNAME
には 2. で指定したブランチ名を入れてください。今回の例ではブランチ名を foo-bar-v1 としていたため以下の様になります。
$ git push -u mine foo-bar-v1
7. GitHub でプルリクエストを作成
プルリクエスト編の「 7. GitHub でプルリクエストを作成」を参考にプルリクエストを作成します。
これで完了です。じきにプルリクエストがマージされるでしょう。 v1 ブランチはセマンティックバージョニングされており、数ヶ月毎に新しいマイナーバージョンがリリースされています。そのルールに従って、今回の修正も次のマイナーバージョンに含まれることになるでしょう。
goa へのプルリクエストの送り方 (プルリクエスト編)
goa へのプルリクエストの送り方 (プルリクエスト編) です。本記事の内容を実施する前に準備編を済ませておいてください。
1. リポジトリに移動
コンソールで以下のコマンドを実行します。
$ cd $GOPATH/src/github.com/goadesign/goa
2. master から新しいブランチをチェックアウト
コンソールで以下のコマンドを実行します。
$ git fetch $ git checkout -b $BRANCHNAME origin/master
$BRANCHNAME
には任意のブランチ名を入れてください。例えばブランチ名を foo-bar とする場合は以下の様になります。
$ git checkout -b foo-bar origin/master
3. ソースコードやテストコードを追加・修正しコミット
コードが書けたら、コンソールで以下のコマンドを実行し、コミットします。
$ git add . $ git commit
もちろん複数回コミットしても構いません。
4. make できることを確認
コンソールで以下のコマンドを実行します。
$ make
これにはプログラムのビルドやテストコードの実行が含まれています。何らかのエラーが発生した場合はコードを修正しそれらを解決します。
5. 最新の master にリベース
コンソールで以下のコマンドを実行します。
$ git fetch $ git rebase origin/master
6. リモートリポジトリにプッシュ
コンソールで以下のコマンドを実行します。
$ git push -u mine $BRANCHNAME
$BRANCHNAME
には 2. で指定したブランチ名を入れてください。今回の例ではブランチ名を foo-bar としていたため以下の様になります。
$ git push -u mine foo-bar
7. GitHub でプルリクエストを作成
- https://github.com/goadesign/goa を開きます。
Branch メニューの右にある New pull request をクリックします。
Compare ページで compare across forks をクリックします。
base fork が goadesign/goa になっていることを確認します。 base branch で master を選択します。
head fork であなたがフォークしたリポジトリを、 compare branch で 6 でプッシュしたブランチを選択します。今回の例では head fork が tchssk/goa 、 compare branch が foo-bar になります。
Title と Description を英語で入力します。
Create pull request ボタンをクリックします。
参考
Creating a pull request from a fork - User Documentation
以上になります。あとはプルリクエストに対してレビューコメントがつくと思うので、議論しながらコードをブラッシュアップしていきましょう。すべて OK だと認めてもらえたらマージしてもらえるはずです!
goa へのプルリクエストの送り方 (準備編)
goa へのプルリクエストの送り方 (準備編) です。本記事の内容は、プルリクエストを送るための準備として最初に一回だけ行う必要があります。
1. GitHub で goa をフォーク
- https://github.com/goadesign/goa を開きます。
ページの右上にある Fork をクリックします。
参考
Fork A Repo - User Documentation
2. goa リポジトリをクローン
コンソールで以下のコマンドを実行します。
$ go get -u github.com/goadesign/goa
3. リポジトリに移動
コンソールで以下のコマンドを実行します。
$ cd $GOPATH/src/github.com/goadesign/goa
4. フォークしたリポジトリをリモートとして追加
コンソールで以下のコマンドを実行します。
$ git remote add mine git@github.com:$USERNAME/goa.git
$USERNAME
には GitHub 上でのあなたのユーザ名を入れて下さい。例えば筆者のユーザ名は tchssk なので以下の様になります。
$ git remote add mine git@github.com:tchssk/goa.git
これで準備は完了です!