exec コマンドに --raw オプションを追加し、生バイナリ出力を正しく扱えるようにした
kamal accessory exec、kamel app exec、kamal server exec が標準出力を SSHKit の capture で取得するため、バイナリデータの末尾の空白や NUL バイトが除去され、データが破損していました。本 PR は --raw オプション を導入し、出力を加工せずそのまま STDOUT に流すことで、バイナリパイプラインを安全に利用できるようにします。
背景
kamal accessory exec がバイナリデータを扱う際に、capture が改行や NUL バイトをトリムしてしまい、PostgreSQL のカスタムアーカイブや tar アーカイブがサイズ減少し復元に失敗する問題が報告されました(#1861)。この現象は標準出力の整形処理がバイナリに不向きであることが根本原因です。バイナリデータをそのまま転送できるオプションが必要と判断され、--raw が実装されました。
技術的な変更
CLI 定義へのオプション追加
lib/kamal/cli/accessory.rb、lib/kamal/cli/app.rb、lib/kamal/cli/server.rb の各 exec コマンドに以下のオプション定義が追記されました。
option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"
このオプションは --interactive(および --detach)と同時に指定できないよう、互換性チェックで排除されています。
with_raw_output ヘルパーの導入
lib/kamal/cli/base.rb に with_raw_output メソッドが追加され、--raw が有効な場合は一時的にログレベルを :error に下げ、コマンド本体の出力だけを残すようにしました。また say メソッドは options[:raw] が有効なときは出力しないよう条件分岐が加えられています。
def with_raw_output(raw, &block)
raw ? KAMAL.with_verbosity(:error, &block) : block.call
end
def say(message = "", * )
super unless options[:raw]
KAMAL.log(message.to_s)
end
puts_by_host のバイナリ対応
lib/kamal/sshkit_with_ext.rb の puts_by_host が raw: 引数を受け取り、raw が true のときは $stdout.binmode でバイナリモードに切り替えて出力をそのまま書き込みます。従来のホスト情報プレフィックスや余分な改行は付加されません。
def puts_by_host(host, output, type: "App", quiet: false, raw: false)
if raw
$stdout.binmode
$stdout.write(output)
else
unless quiet
puts "#{type} Host: #{host}"
end
puts "#{output}\n\n"
end
end
exec 実装へのフラグ反映
各 exec メソッドは raw = options[:raw] を取得し、with_raw_output(raw) do … end でラップされました。capture_with_info の呼び出し時に strip: !raw を渡すことで、--raw 時は出力トリミングを抑制しています。また puts_by_host に raw: raw を渡してバイナリ出力モードへ遷移させています。
with_raw_output(raw) do
pre_connect_if_required
cmd = Kamal::Utils.join_commands(cmd)
hosts = KAMAL.hosts
quiet = options[:quiet]
on(hosts) do |host|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
puts_by_host host, capture_with_info(cmd, strip: !raw), quiet: quiet, raw: raw
end
end
テストの追加
test/cli/server_test.rb と test/integration/app_test.rb に --raw の挙動検証が追加され、バイナリデータがそのまま出力されること、--interactive との併用が例外を投げることが自動テストで保証されています。
設計判断
後方互換性の確保
既存の exec 呼び出しはデフォルトで --raw が無効なため、従来通りの文字列整形ロジックが保持されます。新しいフラグは既存オプションと衝突しないよう option :raw を追加し、互換性チェックで --interactive/--detach と排除しています。これにより既存ユーザーへの影響をゼロにしつつ機能拡張が実現されています。
ロギングレベルの調整
with_raw_output がログレベルを :error に一時的に下げる設計は、バイナリデータがログ文字列と混ざり合うリスクを回避しつつ、非 --raw 実行では従来通りの詳細ログを維持するというトレードオフを選択しています。
最小侵入の実装
変更は主にオプション定義・ヘルパーメソッド・呼び出し側のラップに留まり、ビジネスロジック自体は変更していません。この点がコードベース全体への影響を最小化し、保守性を保ったまま新機能を提供できた理由です。
まとめ
--raw オプションの導入により、kamal exec 系コマンドはバイナリデータをそのままパイプできるようになり、Issue #1861 で報告されたデータ破損が解消されました。実装は後方互換と最小侵入を意識した設計判断に基づき、オプション追加・ログレベル抑制・出力メソッド拡張という三層で実装されています。テストケースも充実しており、今後のバイナリ操作シナリオでも安全に利用できる基盤が整いました。