docker / docker/go-sdk

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

Open Beginner friendly
#155 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
132
Forks
26
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in container/options.go at createExecutableHooks, then inspect Container.Exec in container/container.exec.go to confirm how exit codes and errors are returned. The change is complete when non-zero codes from WithStartupCommand and WithAfterReadyCommand are reported as failures while zero codes and execution errors retain their expected behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, go
Domain
backend-api-design, devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.