GitHub Actions で Haxe コードをビルドするために setup-haxe を作成した
はじめに
Haxe ビルド環境をセットアップするための krdlab/setup-haxe
を作成して Marketplace に公開しました.
背景
仕事の方で Haxe を使っているリポジトリ*1があって,このリポジトリから共通ライブラリが生成されている状況にあるのですが,これまではこの生成作業が手動で行われていました.
しかし作業自体はコマンド化されていること,またリリースのタイミングに規則性があることから自動化が可能であると判断し,関係者と相談した上で GitHub Actions を利用して自動化することにしました.
このとき setup アクションの Haxe 版があると便利なのですが,あいにくと Marketplace ではみつからなかったため,自宅に戻ってから調べて作成してみました.
アクションの作り方
krdlab/setup-haxe
は TypeScript で記述した JavaScript アクションです.公式の情報が大分充実しているため,基本的にはそれほど詰まらずに作成できます.
- https://help.github.com/en/actions/building-actions/creating-a-javascript-action
- https://github.com/actions/typescript-action
- TypeScript で書く場合のベース
- https://github.com/actions/toolkit
- アクションの開発に使う公式のライブラリ
- https://github.com/actions/setup-node
- 実装例の一つとして参考になる
@zeit/ncc
が便利
今回初めて使ったのですが,単純にコードをまとめたいだけならばとても便利でした.ncc build
とするだけで package.json
の main
フィールドを読み取って必要な依存関係をすべてまとめて,dist/index.js
として出力してくれます.
Haxe release asset の解凍
Haxe binaries の *.tar.gz
や *.zip
を解凍すると haxe_20191217082701_67feacebc
のような圧縮ファイル名からは推測しにくい名前のディレクトリが出現します.なので extractTar
や extractZip
の後に exec
で ls
を呼び出してディレクトリ名を解決させています*2.
Windows 環境におけるコマンドの実行
Windows Server 2019 にインストール済のソフトウェア一覧 からはぱっと見分からなかったのですが,実行ログに出力されたパスからすると Git for Windows が入っているみたいです.
あと extract 系の処理は ubuntu-latest
や macos-latest
だとコマンドが実行されるのですが,windows-latest
では PowerShell から .Net のクラスを利用して処理されていました.なるほど.
C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('D:\a\_temp\6253d2a8-f1a0-4596-8e12-7b7dbcf71ad8', 'haxe-3.4.7-win64')" "C:\Program Files\Git\usr\bin\ls.exe" -1 haxe-3.4.7-win64
動作確認の方法
Linux 環境に対する動作確認であれば Docker イメージを利用して簡単に実施可能です.
$ docker run -it --rm -v $PWD:/work ubuntu bash root@xxx:/# cd work #=> 必要なツールがあれば事前に apt-get install root@xxx:/# env INPUT_HAXE-VERSION=4.0.5 node dist/index.js
アクション内部で @actions/core
の getInput
から値を取得している場合は,例えばキーが "haxe-version"
であれば環境変数として INPUT_HAXE-VERSION
を与えるとアクション側のコードでその値が取得できます.
Windows 環境対応がうまくいかない……
現時点で krdlab/setup-haxe
は runs-on: windows-*
に対応していません.以前対応を試みたとき,Haxe コマンドにパスは通るのですが haxelib setup
すると以下のようなエラーが発生してしまうため一旦対応コードを削除しました.
C:\hostedtoolcache\windows\haxe\3.4.7\x64\haxelib.exe setup C:\hostedtoolcache\windows\haxe\3.4.7\x64\lib ##[error]The process 'C:\hostedtoolcache\windows\haxe\3.4.7\x64\haxelib.exe' failed with exit code 3221225595
3221225595
は 0xC000007B
なので STATUS_INVALID_IMAGE_FORMAT
ですかね?ここは日を改めて再調査予定です*3.
おわりに
krdlab/setup-haxe
を作成したことで Haxe コードのビルドを GitHub Actions で手軽に実行できるようになりました.
Windows 環境向けにはまだ問題が残っている状態ですが,いずれは解決したいと考えています.
コマンドラインツールにテンプレート機能を取り入れるための調査と実験
はじめに
Yeoman の generator みたいなテンプレート機能を自前のコマンドラインツール*1に作り込む必要が生じたので調査しました.
結論としては yeoman-environment と mem-fs-editor,inquirer あたりを組み合わせることで容易に実現できました.
Yeomon の調査
今回は Interacting with the file system に記載されている機能の一部 (templates からのファイルコピーと ejs テンプレート処理) が欲しいのでそこらへんを中心に調べます.
ejs によるテンプレート処理については this.fs.copyTpl
を使えば良いことがマニュアルから読み取れます.パッケージは mem-fs-editor のようです.
それからコピー元のパスである templatePath
を解決するための情報については,コードを調べてみると yeoman-environment
がこの辺りの機能を担っていることがわかります.
https://github.com/yeoman/environment/blob/v2.8.0/lib/resolver.js#L42-L76
また,コピー先を決定する destinationPath
は .yo-rc.json
や引数による指定がなければ process.cwd()
から取得しているようです.
https://github.com/yeoman/generator/blob/v4.5.0/lib/index.js#L162-L176
https://github.com/yeoman/generator/blob/v4.5.0/lib/index.js#L831
https://github.com/yeoman/environment/blob/master/lib/environment.js#L209
実験
まずは yeoman-environment
の方から.npm install --save yeoman-environment
して以下のようなコードを書いて実行すると,
// file: /path/to/use-yeoman-environment/index.js const y = require('yeoman-environment'); const env = y.createEnv(); const res = env.lookup({ packagePatterns: 'yeoman-*', filePatterns: '*\/environment.js' }); console.log(res);
$ node index.js [ { generatorPath: '/path/to/use-yeoman-environment/node_modules/yeoman-environment/lib/environment.js', packagePath: '/path/to/use-yeoman-environment/node_modules/yeoman-environment', namespace: 'path:to:use-yeoman-environment:node_modules:yeoman-environment:lib:environment', registered: true } ]
こういう結果が返ってきます.なのでテンプレートデータのコピー元の絶対パス解決には packagePath
を使えば良さそうです.
mem-fs-editor
の方は以下のように利用します.
const memfs = require('mem-fs'); const editor = require('mem-fs-editor'); const fs = editor.create(memfs.create()); const context = { name: 'hoge' }; fs.copyTpl('template/test.json', 'dest/test.json', context); fs.commit(() => console.log('保存した'));
commit
を呼び出すとファイルとして書き出されます.
試作
実際にテンプレートから雛形を生成する試作コマンド proto-cli
を作成します.
proto-cli/ template/ index.js package.json main.js package.json
template/
以下の内容を雛形としてコピーします.template/package.json
の中身は以下の通り.
{ "name": "<%= name %>", "version": "<%= version %>", "description": "<%= description %>", "private": true, "main": "index.js" }
そして main.js
の中身は以下の通り.ユーザー入力は inquirer
でハンドリングします.
#!/usr/bin/env node const yeoman = require('yeoman-environment'); const memfs = require('mem-fs'); const editor = require('mem-fs-editor'); const inquirer = require('inquirer'); const path = require('path'); const fs = editor.create(memfs.create()); const env = yeoman.createEnv(); const res = env.lookup({ packagePatterns: 'proto-cli', filePatterns: 'main.js' }); const templateRoot = path.join(res[0].packagePath, 'template'); const destinationRoot = process.cwd(); const templatePath = file => path.join(templateRoot, file); const destinationPath = file => path.join(destinationRoot, file); const copyTemplate = context => { fs.copyTpl( templatePath('package.json'), destinationPath('package.json'), context ); fs.copy( templatePath('index.js'), destinationPath('index.js') ); fs.commit(() => console.log('initialized')); }; inquirer.prompt([ { name: 'name', message: 'package name', default: path.basename(__dirname) }, { name: 'version', message: 'version', default: '0.1.0' }, { name: 'description', message: 'description' }, { name: 'author', message: 'author' } ]).then(answers => { copyTemplate(answers); });
実際に動くコードはこちら. github.com
npm link
した後,適当なディレクトリの中で proto-cli
を実行すると動作を確認できます.
おわりに
yeoman-environment と mem-fs-editor (+ inquirer) を利用することで期待するテンプレート機能を実装することができました.当初は自前で実装しようと思っていましたが便利な形でパッケージ化されていたため助かりました.
さらにここから commander を組み合わせることで,よく見かける CLI ツールが作成できます.
*1:環境は Node.js
Keycloak を利用して OpenID Connect ライブラリ (haskell-oidc-client) の動作確認をする
はじめに
昨年末に以下の Issue が来ていたことに気付いて修正したのですが,リリース前に Keycloak を用いて haskell-oidc-client の動作確認をできないか試したところ,とても簡単に実施できました.
今回はその方法をまとめたものです.
方法
Keycloak は Docker イメージが公開されています. hub.docker.com
ですから pull して起動すれば,(テスト目的であれば) すぐに利用可能です.
docker pull jboss/keycloak docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=test jboss/keycloak
docker logs -f
を眺めながら起動完了を待った後,localhost:8080
にアクセスするとリダイレクト後に以下の画面が表示されます.Administrator Console からログインします.
これより下で設定手順を説明しますが,あくまでも動作確認を目的とした簡易なものになっているため注意してください.
ログイン後,Clients から Create ボタンを押して Client ID/Secret を発行します.Add Client 画面で項目を埋めて Save し,
Settings タブから Access Type を credential に変更,かつ Valid Redirect URIs を Relying Party のコールバック URL に変更します. 変更を保存すると Credentials タブが出現して Secret を取得できるようになります.
次に Users からログイン用のユーザーを作成します.Email や各種 Name フィールドは設定しておいた方がテストの時に躓きません.パスワードは Save 後の Credentials タブから設定可能です.
またデフォルトでは profile
スコープに name
が含まれていません.Client Scopes から profile を選択し,Mappers タブから name を追加します.
以上で最低限の IdP として動作します.デフォルトの状態で Issuer Location は http://localhost:8080/auth/realms/master です.テストコードに設定して動作確認を開始しましょう.
おわりに
今回の実験でずいぶんと簡単に試せることが分かりました.Admin REST API があるみたいなので,ライブラリの API 仕様テストを自動化できないか試行錯誤したいと思います.
参考情報
daab on kubernetes
はじめに
Raspberry Pi 4 Model B (4GiB) に k3s を導入して複数の試作ボットを single node 上でコンパクトに稼働させられないかな?と考えていて,今回はまずベースとなるマニフェストを Docker Desktop for Mac の kubernetes 上で作成してみます.
要件
構成
daab は hubot ベースなので Brain が Redis を使用します.daab と Redis にそれぞれ Pod を用意してやります.とてもシンプルな構成です.
redis.yml
Redis コンテナは公式の説明に従って --appendonly yes
を指定して起動します.Persitent Volume はとりあえず hostPath でデフォルトの場所 ($HOME/.docker/Volumes
) に作らせておきます*3.これで Pod を更新しても情報が残ります.
後は他の Pod からアクセスするための Service を定義したら完了です.
daab.yml
daab の方は REDIS_URL
を指定し,HUBOT_DIRECT_TOKEN
を Secret から受け取るようにします.また Pod 更新の際に同じトークンを持ったボットが (一時的にでも) 複数稼働するような状況を避けるために .spec.strategy.type = Recreate
にします.
最後に Webhook 用に Ingress を追加*4したら完了です.
起動
あらかじめ ingress-nginx Installation Guide に従って ingress-nginx を導入しておきます*5.
cd daab-ping docker build -t daab-ping:v0.1.3 . daab login #=> アクセストークンを取得する (.env) cd - kubectl create namespace krdlab kubectl create secret generic daab-secrets -n krdlab --from-env-file daab-ping/.env kubectl apply -f redis.yml kubectl apply -f daab.yml kubectl get pods -n krdlab #=> Running を確認
どちらも Running になったらボットが反応してくれるようになります.
おわりに
今回は 1 つのボットを稼働させるために必要なマニフェストを作成しました.この後は k3s 上で動作するように調整を入れて,実装固有の部分をパラメータ化していきます.
GitHub Actions と semantic-releaseを組み合わせて Node.js パッケージのリリースフローを整備する試み
はじめに
オープンソースとして公開しているとある複数のパッケージでリリースフローが整備されいないという問題があって,昨年末は少々頭を悩ませていました.
そこで今回は GitHub Actions と semantic-release を組み合わせて npm へのリリース作業*1を自動化する,という方針で上記の問題を解決できないか?ということを試してみようと思います.
実験台にしたパッケージは krdlab/daab-session *2です.
先に軽く結論を述べますと,ライブラリ系のリポジトリでは導入する価値ありだ (というかした方が良い) と思いました.master ブランチにマージするだけで必要なものが自動的にリリースされるようになります.
GitHub Actions の設定
ここは公式が用意してくれているものをそのまま設定します.この段階では publish 関連を設定せずに,ビルドとテストが成功することだけを確認します.
後ほど少しだけ修正します.
semantic release の設定
semantic-release は Angular Commit Message Conventions にしたがってコミットするとパッケージのバージョニングと公開を自動化してくれます.導入手順は以下の通りです.
- 対象 GitHub リポジトリの Settings にある Secrets へ GitHub と npm のアクセストークンを追加
- semantic-release と必要な plugin を
npm install
- Actions の設定で追加した
.github/workflows/nodejs.yml
に semantic-release 実行ステップを追加 package.json
にrelease
フィールドを追加
1. 対象 GitHub リポジトリの Settings にある Secrets へ GitHub と npm へのアクセストークンを追加
semantic-release が使うトークンとして以下の 2 つが必要になるので,発行 & 設定します.
- GitHub で Personal access token を発行
- repo 権限を与える
- これを
GH_TOKEN
として Secrets に登録する
- npm で Access Token を発行
- publish 権限を与える
- これを
NPM_TOKEN
として Secrets に登録する
2. semantic-release と必要な plugin を npm install
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git
デフォルトでロードされるプラグインは以下の通りです.
https://semantic-release.gitbook.io/semantic-release/usage/plugins#default-plugins
Changelog を生成したい場合は @semantic-release/changelog が追加で必要になります.
また,更新された package.json や生成された CHANGELOG.md はデフォルトだと publish 先にしか含まれないため,リポジトリに反映するためには @semantic-release/git が必要です.
3. .github/workflows/nodejs.yml
に semantic-release 実行ステップを追加
.github/workflows/nodejs.yml
の steps
に以下を追加します.
- name: semantic-release if: matrix.node-version == '12.x' run: | npm run semantic-release env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
matrix build しているため if
でリリースフローが複数回実行されないように絞り込んでおきます.
semantic-release はデフォルト設定だと master ブランチのみを対象としかつ npm を対象とするため,上記の追加だけで「PR を master ブランチにマージすると publish」されるようになります.ちなみに master ブランチ以外で実行した場合は以下のようなメッセージが出力されます.
This test run was triggered on the branch feature/add-github-actions, while semantic-release is configured to only publish from master, therefore a new version won’t be published.
4. package.json
に release
フィールドを追加
package.json
に以下の設定を追加します.といっても plugins
のデフォルト値に changelog と git を追加しているだけです.
"release": { "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/npm", "@semantic-release/github", "@semantic-release/git" ] }
注意点としては,リポジトリに付いている既存のバージョンタグが v1.0.1
のように v
から始まる場合は問題ありませんが,そうでない場合は追加で tagFormat
の設定が必要だということです.
semantic-release のリリースステップや dry-run の出力をみると Git タグを基に次のバージョン値を決めていることが分かります.この時タグからバージョンを抽出するフォーマットが tagFormat
なのですが,このデフォルト値が v${version}
になっているため,これを既存のタグフォーマットにあわせて修正する必要があります.
例えば単純にバージョン値を Git タグとして付けている場合は以下のような設定を追加します.
"release": { "plugins": [ ... ], "tagFormat": "${version}" <--- これ }
補足をいくつか
- dry-run は
GH_TOKEN=<トークン値> npm run semantic-release -- --dry-run
のようにトークンを指定した方が良い- password の入力が不要になる (入力を求められるが空値で良くなる)
- exec プラグインを使って他のパッケージマネージャを対象にすることは可能らしい
- コミットメッセージ規約をサポートするツールもあわせて導入すると良いかも
おわりに
以上のような設定を施すことで「master ブランチへマージしたら Release note や ChangeLog が生成されて npm に publish される」という一連の作業が自動化されます.
実行してみて思ったのは「想像以上に楽だ……」ってことです.パッケージ数が多いとより嬉しいんじゃないかなと思います.
リリース手順は実施頻度が低ければ忘れやすく,頻度が高ければ面倒なものになってしまいます.自動化しておくことでいずれの場合についても負担を軽減できる*3と感じました.
参考情報
Ansible のモジュールを check mode と差分表示に対応させる
はじめに
Ansible の実行時に --check
を指定すると check mode (dry run) として (リモートに変更を加えることなく) 実行され,--diff
を指定すると変更内容の差分が表示されます.
ただしモジュール側がこれらのオプションをサポートしている場合に限ります*1.
check mode や差分表示は事前確認が容易になるという点で利便性が高いため,可能な限りモジュールを対応させたくなります.
これらの対応方法について調べてみると Check Mode (“Dry Run”) で若干触れられていますが,実際には対応済みモジュールのコードを読み解く必要があります*2.
そこで今回はこの対応方法についてまとめておきたいと考えました.
Ansible 環境
ansible 2.6.0 (devel ad69ef88e7) last updated 2018/05/20 18:09:04 (GMT +900) config file = None configured module search path = [u'/Users/krdlab/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /Users/krdlab/.provisioning/dev/projects/misc/krdlab/ansible/lib/ansible executable location = /Users/krdlab/.provisioning/dev/projects/misc/krdlab/ansible/bin/ansible python version = 2.7.11 (default, Oct 20 2016, 16:27:36) [GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)]
ベースとなるサンプルモジュール
path
で指定したファイルを作成/削除するだけのモジュールを用意しました.
https://github.com/krdlab/examples/blob/master/ansible-module-check-and-diff/version1/simplefile.py
以下のように実行します.
$ ansible all -i ',localhost' -c local --module-path version1 -m simplefile -a 'path=/tmp/testfile state=present' localhost | SUCCESS => { "changed": true, "simplefile": { "path": "/tmp/testfile", "state": "present" } }
まだ check mode には対応していないコードであるため,当然 check mode では期待通りに動きません.
$ ansible all -i ',localhost' -c local --module-path version1 -m simplefile -a 'path=/tmp/testfile state=present' --check localhost | SKIPPED # ← スキップされる
check mode のサポート
check mode のサポートを追加したコード全体は,以下のリンク先の通りです.
https://github.com/krdlab/examples/blob/master/ansible-module-check-and-diff/version2/simplefile.py
AnsibleModule
に supports_check_mode = True
を指定すると check mode でもスキップされることなくモジュールが実行されます.
module = AnsibleModule( argument_spec=dict( ... ), supports_check_mode=True # ← これ )
module.check_mode
で check mode かどうかの判定が可能です.
check mode 時は実際の変更をリモートに加えることなく,現在のリモートの状態と指定されたパラメータを比較して「変更が発生するか?」だけを判定させます. 例えば AWS 系のモジュールだと,describe API を呼び出して得られた情報と指定されたパラメータの内容とを比較して "changed" か "ok" かを決定しています.
今回の simplefile
モジュールも (リモートではありませんが) 現在の状態と指定されたパラメータに基づいて,変更が発生する場合に changed = True
を返すように実装しています (以下のリンク先参照).
また戻り値*3をきちんと返すようにすると,register
を使っていても後続のタスクが失敗しなくなるためおすすめです*4.
差分表示のサポート
差分表示のサポートを追加したコード全体は,以下のリンク先の通りです.
https://github.com/krdlab/examples/blob/master/ansible-module-check-and-diff/version3/simplefile.py
module.exit_json
に diff
パラメータを渡すと,--diff
を指定して実行した場合に変更差分が表示されるようになります.
...
module.exit_json(changed=changed, diff=diff, simplefile=params)
# ↑これ
diff の構造は以下のものが期待されます.
diff = dict( before=dict( 変更前の状態 ), after=dict( 変更後の状態 ) )
before
と after
の中身は比較可能となるようにモジュール作成者が決定します.
user
や file
モジュールなどを参考に実装すると良さそうです.
今回の simplefile
モジュールだと以下のような情報を返しています.
def __create_diff(self): path = self.expected['path'] act_state, exp_state = self.__file_state(), self.expected['state'] diff = dict(before=dict(path=path), after=dict(path=path)) # path は変化しないので必ず入れる if act_state != exp_state: # state は変化する場合だけ diff に取り込む diff['before']['state'] = act_state diff['after']['state'] = exp_state return diff
上記のようなデータを返すと,--diff
を付けて実行したときに以下のような表示が得られます.ちなみにこれは,既に存在している /tmp/testfile
を absent
にする場合のものです.
--- before +++ after @@ -1,4 +1,4 @@ { "path": "/tmp/testfile", - "state": "present" + "state": "absent" }
おわりに
Ansible のモジュールを check mode に対応させることで,実際に変更を適用する前に変更が発生するのかどうかを知ることができます.
差分表示にまで対応させれば「あれ?なんの変更だっけ?」といったときに役立ちます.
また可能であれば check mode でも戻り値を返すようにしておくと,regsiter
を使ってい他場合に後続タスクが失敗しなくなります.
やはり適用前に確認はしたいですから,check mode に対応することは結構重要なことだと思います*5.
というわけで今回はその対応方法の紹介でした.
*1:実際に Ansible の標準モジュールの中にも未対応なものがちらほらみつかります.コアモジュール以外は "not required" とされているようです → Check Mode (“Dry Run”)
*2:もしかしてどこかにまとまっているんだろうか?
*3:ドキュメントで Return Value として定義されているデータ
*4:どうしても返せない場合もあるのですが……
*5:なのでこれが取り込まれると嬉しいなぁ…… → https://github.com/ansible/ansible/pull/39846
Servant に Experimental モジュールとして追加された認証の仕組み
はじめに
久しぶりに Haskell に戻って Servant を触ってみたら,いつの間にか認証周りの仕組みが実験的に追加されていました. 興味がわいたため,Authentication in Servant とコードを読みながら少しだけ仕組みをのぞいてみました.
バージョン情報
lts-7.14
です.バージョンは以下の通りです.
- GHC 8.0.1
- servant 0.8.1
サンプルコード
実際に動くコードを用意しました.
Recap の方にまとまっていますが,認証無しの場合と異なる箇所をザックリ挙げると以下の通りです.
- 認証をかけたいエンドポイントに
AuthProtect
コンビネーターを指定するAuthProtect "hoge" :> ProtectedAPI
とした場合,ProtectedAPI
が認証処理で保護される
AuthProtect tag
と対応した type familyAuthServerData
の型インスタンスを宣言する- 認証ハンドラが返す値の具体的な型を
HasServer api context
インスタンスに指定する役割
- 認証ハンドラが返す値の具体的な型を
serve
の代わりにserveWithContext
を使用する- ここで
HasServer api context
のcontext
を指定
- ここで
Context
には実際の認証処理を担うAuthHandler
を積むAuthHandler
は認証処理関数 (r -> Handler usr
) をラップしたもの
少しだけ仕組みを覗いてみる
AuthProtect
コンビネーターを指定すると以下のルーティングが有効になります.
-- 引用: https://hackage.haskell.org/package/servant-server-0.8.1/docs/src/Servant.Server.Experimental.Auth.html#line-54 instance ( HasServer api context , HasContextEntry context (AuthHandler Request (AuthServerData (AuthProtect tag))) ) => HasServer (AuthProtect tag :> api) context where type ServerT (AuthProtect tag :> api) m = AuthServerData (AuthProtect tag) -> ServerT api m route Proxy context subserver = route (Proxy :: Proxy api) context (subserver `addAuthCheck` withRequest authCheck) where authHandler :: Request -> Handler (AuthServerData (AuthProtect tag)) authHandler = unAuthHandler (getContextEntry context) authCheck :: Request -> DelayedIO (AuthServerData (AuthProtect tag)) authCheck = (>>= either delayedFailFatal return) . liftIO . runExceptT . authHandler -- 以下はコードを読むための補足: -- type Server api = ServerT api Handler -- type Handler = ExceptT ServantErr IO
HasContextEntry
により,HasServer api context
の context
からは getContextEntry
を使って AuthHandler Request (AuthServerData (AuthProtect tag))
を取得することが出来ます (ということが型制約から要求されます).
context
は serverWithContext
で指定した型で,先のサンプルコードでは AuthHandler
を積んだ型レベルのリストです.
AuthServerData (AuthProtect tag)
の部分には (先のサンプルコード中にある) type family のインスタンス宣言によって Account
が対応します.
また関連型により以下のような対応付けがなされるため,
type ServerT (AuthProtect tag :> api) m = AuthServerData (AuthProtect tag) -> ServerT api m
subserver
は Delayed env (Account -> ServerT api Handler a)
という型を持つことになります (Delayed
については後述).
これにより,対応するハンドラは Account
を引数として受けることが要求されます.
後は取り出した authHandler
を実行してその結果を subserver
に渡すだけですが,すぐには実行されず一旦 Delayed
の形で積まれて後続の処理へと渡されます*1.
-- 引用: https://hackage.haskell.org/package/servant-server-0.8.1/docs/src/Servant.Server.Internal.RoutingApplication.html#addAuthCheck addAuthCheck :: Delayed env (a -> b) -> DelayedIO a -> Delayed env b addAuthCheck Delayed{..} new = Delayed { authD = (,) <$> authD <*> new , serverD = \ c (y, v) b req -> ($ v) <$> serverD c y b req , .. } -- Note [Existential Record Update]
Delayed
はエラーハンドリングの順序を決定するための仕組みだそうです.以下のような順序で実行され,すべてパスした場合に実際のハンドラが呼び出されます.
-- 引用: https://hackage.haskell.org/package/servant-server-0.8.1/docs/src/Servant.Server.Internal.RoutingApplication.html#runDelayed runDelayed :: Delayed env a -> env -> Request -> IO (RouteResult a) runDelayed Delayed{..} env = runDelayedIO $ do c <- capturesD env methodD a <- authD b <- bodyD DelayedIO (\ req -> return $ serverD c a b req)
runDelayed
は runAction
から呼び出されます.
-- 引用: https://hackage.haskell.org/package/servant-server-0.8.1/docs/src/Servant.Server.Internal.RoutingApplication.html#runAction runAction :: Delayed env (Handler a) -> env -> Request -> (RouteResult Response -> IO r) -> (a -> RouteResult Response) -> IO r runAction action env req respond k = runDelayed action env req >>= go >>= respond where go (Fail e) = return $ Fail e go (FailFatal e) = return $ FailFatal e go (Route a) = do e <- runExceptT a -- Handler a の結果を取り出している case e of Left err -> return . Route $ responseServantErr err Right x -> return $! k x
runAction
は ... :> Get '[JSON] Text
のような末端を処理する route
関数から呼び出されます.これで無事レスポンスが生成されました.
servant-auth (servant-auth-server) という別の選択肢
AuthProtect
を使わずに認証周りを実装したパッケージとして servant-auth があります.
Auth (auths :: [*]) val
というコンビネーターが用意されており,auths に複数の認証方式を指定できるようになっています.
このパッケージでは JWT
と Cookie
が提供されています.
どちらも暗号化したデータを Token や Set-Cookie に用いる実装のようです.
おわりに
AuthProtect の仕組みによって認証を型として指定できるようになりました. セッション ID とユーザーの紐付けや,API トークンベースの認証が実装しやすくなりました.
Context
のあたりは他にも WithNamedContext といった仕組みがあるようです.
しかし巧妙な方法ですね.自分ではとても思いつけそうにありません.
*1:ここら辺も結構変更されていますね