サービスオプションのサーバ側バリデーション追加

basecamp/kamal-proxy

本PRは、TLS設定時のホスト必須や canonical‑host 整合性など、サービスオプションの整合性チェックをサーバ側に統一し、Validate メソッドと共通エラー型 ErrServiceOptionsInvalid を導入した。これにより、CLI の事前チェックと同等の検証が API 経由でも保証されるようになった。

背景

ServiceOptions のバリデーションが CLI の preRun にのみ実装されていたため、サーバ側 API から直接デプロイを呼び出すケースで無効な設定が通過するリスクが残っていた。TLS 有効化時にホストが未設定だったり、canonical-hosthosts 配列に含まれない状態は、実行時エラーや予期せぬリダイレクトにつながる可能性があった。Issue やテストの不足が指摘され、検証ロジックを単一の場所に集約する方針が取られた。

技術的な変更

Validate メソッドの追加 により、ServiceOptions が自身の整合性を判断できるようになった。メソッドはまず Normalize() を呼び出し、次に TLS が有効な場合は HasConfiguredHosts() とルートパス ("/") の存在を確認し、canonical-host のチェックも行う。失敗時は ErrServiceOptionsInvalid をラップした fmt.Errorf を返す。

func (so ServiceOptions) Validate() error {
    so.Normalize()

    if so.TLSEnabled {
        if !so.HasConfiguredHosts() {
            return fmt.Errorf("%w: host must be set when using TLS", ErrServiceOptionsInvalid)
        }
        if !slices.Contains(so.PathPrefixes, rootPath) {
            return fmt.Errorf("%w: TLS settings must be specified on the root path service", ErrServiceOptionsInvalid)
        }
    }

    if so.CanonicalHost != "" && len(so.Hosts) > 0 && so.Hosts[0] != "" {
        if !slices.Contains(so.Hosts, so.CanonicalHost) {
            return fmt.Errorf("%w: canonical-host '%s' must be present in the hosts list: %v", ErrServiceOptionsInvalid, so.CanonicalHost, so.Hosts)
        }
    }
    return nil
}

ErrServiceOptionsInvalid の導入 は、バリデーション失敗を呼び出し元で型判定できるようにするための sentinel エラーである。internal/server/service.go のエラーレジストリに新規追加され、他コンポーネントでも同一エラーを参照できる。

var (
    // ... 既存エラー省略 ...
    ErrServiceOptionsInvalid = errors.New("service options invalid")
)

CLI の preRun は従来の手動チェックを削除し、c.args.ServiceOptions.Validate() を呼び出すだけに簡素化された。これにより、ロジックの二重記述が解消され、テスト対象が一元化された。

func (c *deployCommand) preRun(cmd *cobra.Command, args []string) error {
    if err := c.args.ServiceOptions.Validate(); err != nil {
        return err
    }
    // 既存のフラグ整合性チェックはそのまま残す
    return nil
}

Router.DeployService でも options.Validate() が呼び出され、リクエスト受信直後にバリデーションが行われるようになった。これにより、HTTP API 経由のデプロイでも安全性が確保された。

func (r *Router) DeployService(name string, targetURLs, readerURLs []string, options ServiceOptions, targetOptions TargetOptions, deploymentOptions DeploymentOptions) error {
    if err := options.Validate(); err != nil {
        return err
    }
    options.Normalize()
    slog.Info("Deploying", "service", name, "targets", targetURLs, "hosts", options.Hosts, "paths", options.PathPrefixes, "tls", options.TLSEnabled)
    // 以降のロジックは変更なし
    ...
}

テストの拡充 として、internal/server/commands_test.gointernal/server/router.gointernal/server/service_test.gointernal/cmd/deploy_test.go にそれぞれバリデーション失敗ケースを検証するテストが追加された。特に TestCommandHandler_DeployRejectsInvalidServiceOptions は、TLS 有効かつホスト未設定でエラーが返ることを require.ErrorIs で確認している。

func TestCommandHandler_DeployRejectsInvalidServiceOptions(t *testing.T) {
    handler := NewCommandHandler(testRouter(t))
    var result bool
    err := handler.Deploy(DeployArgs{ServiceOptions: ServiceOptions{TLSEnabled: true}}, &result)
    require.ErrorContains(t, err, "host must be set when using TLS")
    require.ErrorIs(t, err, ErrServiceOptionsInvalid)
}

サーバテストの微調整 では、TLS テストケースで serviceOptions.Hosts に有効なホストを設定し、実際に HTTPS デプロイが成功することを確認した。これにより、バリデーションロジックが期待通りに機能することがエンドツーエンドで保証された。

serviceOptions := defaultServiceOptions
serviceOptions.Hosts = []string{"localhost"}
serviceOptions.TLSEnabled = true
serviceOptions.TLSCertificatePath = certPath
serviceOptions.TLSPrivateKeyPath = keyPath

設計判断

バリデーションを ServiceOptions に集約し、Validate を単一の公開インタフェースとして提供したのは、同一の整合性チェックを複数箇所で再実装するリスクを排除するための設計判断である。{{ErrServiceOptionsInvalid}} を sentinel エラーとして導入したことで、呼び出し側はエラー種別を errors.Is で判定でき、エラーメッセージとエラー種別を分離できる点が利点となる。

この変更は、既存の Normalize 呼び出し順序を維持しつつ Validate の内部で正規化を行うため、後方互換性 を犠牲にしない。CLI とサーバ双方で同一ロジックが走ることで、設定ミスが早期に検出され、運用上の安全性が向上した。

まとめ

本PRは、サービスオプションの整合性チェックをサーバ側に導入し、共通の Validate メソッドと ErrServiceOptionsInvalid エラーで一元管理した。これにより、CLI と API の双方で同一のバリデーションが保証され、テストカバレッジも拡充されたことで、TLS 設定時の構成ミスが防止されるようになった。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
1c54e6b7

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断、まとめの5部構成があり、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きシンタックスハイライトは正しいが、PRリンクの記法が[PR #219](URL)となっており、仕様の[#219](URL)形式と異なるためWARNINGとなります。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的な用語で記述されており、エンジニア向けとして適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論の段落構成になっており、トピックセンテンスで始まり、段落長も適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックはDiffに示された変更と一致しており、ファイル名・内容とも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語はPRと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的説明はDiffとPR内容に基づき正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張はPR情報で裏付けられ、外部知識や推測はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #219 が正しく記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容を日本語で適切に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

外部知識は含まれておらず、PR情報のみで記述されています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現は使用されておらず、PRと矛盾はありません。