Skip to content

Commit c1a5f75

Browse files
mdelapenyaclaude
andauthored
feat(wait): add human-readable String() methods to all wait strategies (docker#119)
* feat(wait): add human-readable String() methods to all wait strategies Add String() method to all wait strategy types to provide human-readable descriptions that are displayed in container lifecycle logs. This improves the user experience by showing clear wait conditions instead of raw struct output. Changes: - Add String() method to HTTPStrategy, HealthStrategy, LogStrategy, HostPortStrategy, FileStrategy, ExecStrategy, ExitStrategy, waitForSQL, MultiStrategy, NopStrategy, and TLSStrategy - Update lifecycle logging to use strategy String() output - Each strategy provides contextual information (ports, paths, conditions) Example output changes: Before: Waiting for: &{timeout:<nil> deadline:0x140001c7190 Strategies:[0x1400032e1d0]} After: Waiting for container to be ready strategy="HTTP GET request on port 8080 path /health" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: use switch * chore: wrap URL path * fix(wait): address linter feedback for String() methods - Use switch statement instead of if-else chain in HostPortStrategy - Quote path in HTTPStrategy output for clarity - Use Port.Port() method instead of string cast for nat.Port 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(wait): prevent sensitive data exposure in ExecStrategy String() Only show command name and argument count instead of full command with arguments, which may contain passwords, tokens, or other credentials. Example: "exec command \"mysql\" with 4 argument(s)" instead of ["mysql", "-u", "root", "-pSecretPass", "mydb"] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(wait): always show MultiStrategy wrapper in String() output Always include "all of:" prefix even when there's only one strategy after filtering out nils, to make it clear that a MultiStrategy wrapper is being used. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(wait): use proper singular/plural grammar in ExecStrategy String() Use "argument" (singular) when argCount is 1, and "arguments" (plural) otherwise for grammatically correct output. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 9f4ef34 commit c1a5f75

File tree

12 files changed

+159
-2
lines changed

12 files changed

+159
-2
lines changed

container/lifecycle.create.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,13 @@ var defaultReadinessHook = func() LifecycleHooks {
9696

9797
// if a Wait Strategy has been specified, wait before returning
9898
if waiter.WaitingFor() != nil {
99-
c.Logger().Info("Waiting for container to be ready", "containerID", c.ShortID(), "image", c.Image())
100-
if err := waiter.WaitingFor().WaitUntilReady(ctx, waiter); err != nil {
99+
strategy := waiter.WaitingFor()
100+
strategyDesc := "unknown strategy"
101+
if s, ok := strategy.(fmt.Stringer); ok {
102+
strategyDesc = s.String()
103+
}
104+
c.Logger().Info("Waiting for container to be ready", "containerID", c.ShortID(), "image", c.Image(), "strategy", strategyDesc)
105+
if err := strategy.WaitUntilReady(ctx, waiter); err != nil {
101106
return fmt.Errorf("wait until ready: %w", err)
102107
}
103108
}

container/wait/all.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package wait
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"reflect"
8+
"strings"
79
"time"
810
)
911

@@ -44,6 +46,29 @@ func (ms *MultiStrategy) Timeout() *time.Duration {
4446
return ms.timeout
4547
}
4648

49+
// String returns a human-readable description of the wait strategy.
50+
func (ms *MultiStrategy) String() string {
51+
if len(ms.Strategies) == 0 {
52+
return "all of: (none)"
53+
}
54+
55+
var strategies []string
56+
for _, strategy := range ms.Strategies {
57+
if strategy == nil || reflect.ValueOf(strategy).IsNil() {
58+
continue
59+
}
60+
if s, ok := strategy.(fmt.Stringer); ok {
61+
strategies = append(strategies, s.String())
62+
} else {
63+
strategies = append(strategies, fmt.Sprintf("%T", strategy))
64+
}
65+
}
66+
67+
// Always include "all of:" prefix to make it clear this is a MultiStrategy
68+
// even when there's only one strategy after filtering out nils
69+
return "all of: [" + strings.Join(strategies, ", ") + "]"
70+
}
71+
4772
func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
4873
var cancel context.CancelFunc
4974
if ms.deadline != nil {

container/wait/exec.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package wait
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67
"time"
78

@@ -76,6 +77,22 @@ func (ws *ExecStrategy) Timeout() *time.Duration {
7677
return ws.timeout
7778
}
7879

80+
// String returns a human-readable description of the wait strategy.
81+
func (ws *ExecStrategy) String() string {
82+
if len(ws.cmd) == 0 {
83+
return "exec command"
84+
}
85+
// Only show the command name and argument count to avoid exposing sensitive data
86+
argCount := len(ws.cmd) - 1
87+
if argCount == 0 {
88+
return fmt.Sprintf("exec command %q", ws.cmd[0])
89+
}
90+
if argCount == 1 {
91+
return fmt.Sprintf("exec command %q with 1 argument", ws.cmd[0])
92+
}
93+
return fmt.Sprintf("exec command %q with %d arguments", ws.cmd[0], argCount)
94+
}
95+
7996
func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
8097
timeout := defaultTimeout()
8198
if ws.timeout != nil {

container/wait/exit.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (ws *ExitStrategy) Timeout() *time.Duration {
5959
return ws.timeout
6060
}
6161

62+
// String returns a human-readable description of the wait strategy.
63+
func (ws *ExitStrategy) String() string {
64+
return "container to exit"
65+
}
66+
6267
// WaitUntilReady implements Strategy.WaitUntilReady
6368
func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
6469
if ws.timeout != nil {

container/wait/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ func (ws *FileStrategy) Timeout() *time.Duration {
6161
return ws.timeout
6262
}
6363

64+
// String returns a human-readable description of the wait strategy.
65+
func (ws *FileStrategy) String() string {
66+
if ws.matcher != nil {
67+
return fmt.Sprintf("file %q to exist and match condition", ws.file)
68+
}
69+
return fmt.Sprintf("file %q to exist", ws.file)
70+
}
71+
6472
// WaitUntilReady waits until the file exists in the container and copies it to the target.
6573
func (ws *FileStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
6674
timeout := defaultTimeout()

container/wait/health.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ func (ws *HealthStrategy) Timeout() *time.Duration {
6060
return ws.timeout
6161
}
6262

63+
// String returns a human-readable description of the wait strategy.
64+
func (ws *HealthStrategy) String() string {
65+
return "container to become healthy"
66+
}
67+
6368
// WaitUntilReady implements Strategy.WaitUntilReady
6469
func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
6570
timeout := defaultTimeout()

container/wait/host_port.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ func (hp *HostPortStrategy) Timeout() *time.Duration {
113113
return hp.timeout
114114
}
115115

116+
// String returns a human-readable description of the wait strategy.
117+
func (hp *HostPortStrategy) String() string {
118+
port := "first exposed port"
119+
if hp.Port != "" {
120+
port = fmt.Sprintf("port %s", hp.Port)
121+
}
122+
123+
var checks string
124+
switch {
125+
case hp.skipInternalCheck && hp.skipExternalCheck:
126+
checks = " to be mapped"
127+
case hp.skipInternalCheck:
128+
checks = " to be accessible externally"
129+
case hp.skipExternalCheck:
130+
checks = " to be listening internally"
131+
default:
132+
checks = " to be listening"
133+
}
134+
135+
return fmt.Sprintf("%s%s", port, checks)
136+
}
137+
116138
// detectInternalPort returns the lowest internal port that is currently bound.
117139
// If no internal port is found, it returns the zero nat.Port value which
118140
// can be checked against an empty string.

container/wait/http.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ func (ws *HTTPStrategy) Timeout() *time.Duration {
163163
return ws.timeout
164164
}
165165

166+
// String returns a human-readable description of the wait strategy.
167+
func (ws *HTTPStrategy) String() string {
168+
proto := "HTTP"
169+
if ws.UseTLS {
170+
proto = "HTTPS"
171+
}
172+
173+
port := "default"
174+
if ws.Port != "" {
175+
port = ws.Port.Port()
176+
}
177+
178+
return fmt.Sprintf("%s %s request on port %s path %q", proto, ws.Method, port, ws.Path)
179+
}
180+
166181
// WaitUntilReady implements Strategy.WaitUntilReady
167182
func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
168183
timeout := defaultTimeout()

container/wait/log.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ func (ws *LogStrategy) Timeout() *time.Duration {
123123
return ws.timeout
124124
}
125125

126+
// String returns a human-readable description of the wait strategy.
127+
func (ws *LogStrategy) String() string {
128+
logType := "log message"
129+
if ws.IsRegexp {
130+
logType = "log pattern"
131+
}
132+
133+
occurrence := ""
134+
if ws.Occurrence > 1 {
135+
occurrence = fmt.Sprintf(" (occurrence: %d)", ws.Occurrence)
136+
}
137+
138+
return fmt.Sprintf("%s %q%s", logType, ws.Log, occurrence)
139+
}
140+
126141
// WaitUntilReady implements Strategy.WaitUntilReady
127142
func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
128143
timeout := defaultTimeout()

container/wait/nop.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ func (ws *NopStrategy) Timeout() *time.Duration {
3434
return ws.timeout
3535
}
3636

37+
// String returns a human-readable description of the wait strategy.
38+
func (ws *NopStrategy) String() string {
39+
return "custom wait condition"
40+
}
41+
3742
func (ws *NopStrategy) WithTimeout(timeout time.Duration) *NopStrategy {
3843
ws.timeout = &timeout
3944
return ws

0 commit comments

Comments
 (0)