WithStartupCommand silently swallows non-zero exit codes from the inner command

オープン 初心者向け
#155 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

@locker95 がすでに取り組んでいます。

2026年10月28日 から。

  • #174 @locker95 による — オープン
  • #175 @kw-00 による — オープン

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
76/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
静か
技術スタック
docker, go

調査の方向性

container/options.go の createExecutableHooks から始め、次に container/container.exec.go の Container.Exec を調べて、終了コードとエラーがどのように返されるかを確認します。WithStartupCommand と WithAfterReadyCommand のゼロ以外のコードが失敗として報告され、ゼロのコードと実行エラーが期待される動作を維持すれば、変更は完了です。

索引モデルが issue の本文から書いたものです。

説明

Summary

container.WithStartupCommand registers a PostStarts lifecycle hook that runs each Executable via Container.Exec. When the inner command exits non-zero, the hook returns nil and container.Run reports success. The container ends up in a "running but the startup command failed" state with no visible signal to the caller.

Where

  • The hook is built in container/options.go (createExecutableHooks):

    execFn := func(ctx context.Context, c ContainerInfo) error {
        if executor, ok := c.(ContainerExecutor); ok {
            _, _, err := executor.Exec(ctx, exec.AsCommand(), exec.Options()...)
            return err
        }
        return errors.New("container does not support execution")
    }
    
  • Container.Exec in container/container.exec.go returns (int, io.Reader, error). The first return is the inner process's exit code — a value, not an error. The hook discards it.

Reproduction

c, err := container.Run(ctx,
    container.WithImage("alpine:latest"),
    container.WithEntrypoint("tail", "-f", "/dev/null"),
    container.WithStartupCommand(exec.NewRawCommand([]string{"false"})),
)
// err == nil; c is up; the failing startup command is invisible.

This bites consumers in two ways:

  1. First-create coverage doesn't fail loudly. A startup command that errors is silently ignored at create time. The caller has no way to discover the failure short of checking side effects.
  2. The same trap applies to WithAfterReadyCommand (same hook factory).

I hit this while writing tests for WithDurableStartupCommand (#154): a WithUser("nonexistent") case where su aborts, the rendered dispatcher exits non-zero, but Run still succeeds. I had to invoke the dispatcher directly via c.Exec and check the exit code value to observe the failure.

Suggested fix

Have createExecutableHooks (or a thin helper) treat a non-zero exit code as an error:

execFn := func(ctx context.Context, c ContainerInfo) error {
    executor, ok := c.(ContainerExecutor)
    if !ok {
        return errors.New("container does not support execution")
    }
    code, _, err := executor.Exec(ctx, exec.AsCommand(), exec.Options()...)
    if err != nil {
        return err
    }
    if code != 0 {
        return fmt.Errorf("startup command %v exited with code %d", exec.AsCommand(), code)
    }
    return nil
}

This is a behavioural change for any caller that today relies on startup-command failures being invisible — but I'd expect that population to be small, and the silent-failure mode is more dangerous than the loud one.

If a strictly backwards-compatible variant is preferred, an opt-in option (WithStrictStartupCommand?) could keep the current behaviour as the default.

Out of scope (but related)

  • Container.Exec's signature returning (int, io.Reader, error) makes the exit code easy to ignore. A future API revision could fold non-zero into the error return; for now, fixing the hook is the smaller change.
主要言語
Go
スター
133
フォーク
28
平均マージ
3時間 55分
マージ済み PR(30日)
4

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

docker/go-sdk のほかの issue

docker/go-sdk の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。