1package deployment
2
3import (
4 "bufio"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "net/http"
11 "strings"
12 "time"
13)
14
15// probeHTTPClient is the dedicated readiness client. It never consults
16// environment proxy settings, never follows redirects, and bounds every
17// request so a nonresponding workload cannot hold the probe open.
18var probeHTTPClient = &http.Client{
19 Transport: &http.Transport{Proxy: nil},
20 CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
21 return http.ErrUseLastResponse
22 },
23 Timeout: 5 * time.Second,
24}
25
26// httpGetOnce performs one HTTP GET without following redirects. It dials
27// probeURL's authority (the loopback published bind) while sending
28// hostAuthority as the Host header, so a workload that validates the Host
29// port of its declared container port accepts the probe.
30func httpGetOnce(ctx context.Context, probeURL, hostAuthority string) (int, error) {
31 req, err := http.NewRequestWithContext(ctx, http.MethodGet, probeURL, nil)
32 if err != nil {
33 return 0, err
34 }
35 if hostAuthority != "" {
36 req.Host = hostAuthority
37 }
38 resp, err := probeHTTPClient.Do(req)
39 if err != nil {
40 return 0, err
41 }
42 defer func() { _ = resp.Body.Close() }()
43 return resp.StatusCode, nil
44}
45
46// tcpSettleWindow is how long an accepted TCP probe connection must stay open
47// (or deliver data) before the target counts as ready.
48const tcpSettleWindow = 300 * time.Millisecond
49
50// errTCPProbeClosed reports a connection that was accepted and then closed
51// within the settle window.
52var errTCPProbeClosed = errors.New("connection closed right after accept")
53
54// tcpDialOnce attempts one TCP readiness check against a published loopback
55// bind. A successful connect alone proves nothing: the host-side port relay
56// (rootless Docker, docker-proxy) accepts on behalf of the container and then
57// closes at once while the workload is not listening yet. So the connection
58// must also survive tcpSettleWindow: data from the workload (a banner) or a
59// connection still open at the end of the window means ready; EOF or a reset
60// means not ready.
61func tcpDialOnce(ctx context.Context, addr string) error {
62 return tcpProbeOnce(ctx, addr, tcpSettleWindow)
63}
64
65func tcpProbeOnce(ctx context.Context, addr string, settle time.Duration) error {
66 dialer := &net.Dialer{Timeout: 2 * time.Second}
67 conn, err := dialer.DialContext(ctx, "tcp", addr)
68 if err != nil {
69 return err
70 }
71 defer func() { _ = conn.Close() }()
72 if err := conn.SetReadDeadline(time.Now().Add(settle)); err != nil {
73 return fmt.Errorf("set probe read deadline: %w", err)
74 }
75 stop := context.AfterFunc(ctx, func() { _ = conn.SetReadDeadline(time.Now()) })
76 defer stop()
77 var buf [1]byte
78 n, err := conn.Read(buf[:])
79 if ctxErr := ctx.Err(); ctxErr != nil {
80 return ctxErr
81 }
82 if n > 0 {
83 return nil
84 }
85 var netErr net.Error
86 if errors.As(err, &netErr) && netErr.Timeout() {
87 return nil
88 }
89 if err == nil || errors.Is(err, io.EOF) {
90 return errTCPProbeClosed
91 }
92 return err
93}
94
95// scanForMarker reports whether the stream contains the marker.
96func scanForMarker(stream io.ReadCloser, marker string) (bool, error) {
97 scanner := bufio.NewScanner(stream)
98 scanner.Buffer(make([]byte, 64*1024), 1024*1024)
99 for scanner.Scan() {
100 if strings.Contains(scanner.Text(), marker) {
101 return true, nil
102 }
103 }
104 if err := scanner.Err(); err != nil {
105 return false, fmt.Errorf("deployment: read logs: %w", err)
106 }
107 return false, nil
108}