サービスオプションのサーバ側バリデーション追加
本PRは、TLS設定時のホスト必須や canonical‑host 整合性など、サービスオプションの整合性チェックをサーバ側に統一し、Validate メソッドと共通エラー型 ErrServiceOptionsInvalid を導入した。これにより、CLI の事前チェックと同等の検証が API 経由でも保証されるようになった。
背景
ServiceOptions のバリデーションが CLI の preRun にのみ実装されていたため、サーバ側 API から直接デプロイを呼び出すケースで無効な設定が通過するリスクが残っていた。TLS 有効化時にホストが未設定だったり、canonical-host が hosts 配列に含まれない状態は、実行時エラーや予期せぬリダイレクトにつながる可能性があった。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.go、internal/server/router.go、internal/server/service_test.go、internal/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 設定時の構成ミスが防止されるようになった。