-
Notifications
You must be signed in to change notification settings - Fork 687
/
envoy.go
146 lines (121 loc) · 4.65 KB
/
envoy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright 2020 Datawire. All rights reserved.
//
// package acp contains stuff dealing with the Ambassador Control Plane as a whole.
//
// This is the EnvoyWatcher, which is a class that can keep an eye on a running
// Envoy - and just Envoy, all other Ambassador elements are ignored - and tell you
// whether it's alive and ready, or not.
//
// At the moment, "alive" and "ready" mean the same thing for an EnvoyWatcher. Both
// IsAlive() and IsReady() methods exist, though, for a future in which we monitor
// them separately.
//
// TESTING HOOKS:
// Since we try to check Envoy readiness to see how Envoy is doing, you can use
// EnvoyWatcher.SetReadyCheck to change the function that EnvoyWatcher uses to
// check readiness. The default is EnvoyWatcher.defaultFetcher, which tries to pull
// readiness from http://localhost:8001/ready.
//
// This hook is NOT meant for you to change the fetcher on the fly in a running
// EnvoyWatcher. Set it at instantiation, then leave it alone. See envoy_test.go
// for more.
package acp
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/datawire/dlib/dlog"
)
// EnvoyWatcher encapsulates state and methods for keeping an eye on a running
// Envoy, and deciding if it's healthy.
type EnvoyWatcher struct {
// This mutex is mostly rank paranoia, since we've really only the one
// data element at this point...
mutex sync.Mutex
// How shall we determine Envoy's readiness?
readyCheck envoyFetcher
// Did the last ready check succeed?
LastSucceeded bool
}
// NewEnvoyWatcher creates a new EnvoyWatcher, given a fetcher.
func NewEnvoyWatcher() *EnvoyWatcher {
w := &EnvoyWatcher{}
w.SetReadyCheck(w.defaultFetcher)
return w
}
// This the default Fetcher for the EnvoyWatcher -- it actually connects to Envoy
// and checks for ready.
func (w *EnvoyWatcher) defaultFetcher(ctx context.Context) (*EnvoyFetcherResponse, error) {
// Set up a context with a deliberate 2-second timeout. Envoy shouldn't ever take more
// than 100ms to answer the ready check, and if we don't pick a short timeout here,
// this call can hang for way longer than we would like it to.
tctx, tcancel := context.WithTimeout(ctx, 2*time.Second)
defer tcancel()
// Build a request...
req, err := http.NewRequestWithContext(tctx, http.MethodGet, "http://localhost:8001/ready", nil)
if err != nil {
// ...which should never fail. WTFO?
return nil, fmt.Errorf("error creating request: %v", err)
}
// We were able to create the request, so now fire it off.
resp, err := http.DefaultClient.Do(req)
if err != nil {
// Unlike the last error case, this one isn't a weird situation at
// all -- e.g. if Envoy isn't running yet, we'll land here.
return nil, fmt.Errorf("error fetching /ready: %v", err)
}
// Don't forget to close the body once done.
defer resp.Body.Close()
// We're going to return the status code and the response body, so we
// need to grab those.
statusCode := resp.StatusCode
text, err := ioutil.ReadAll(resp.Body)
if err != nil {
// This is a bit strange -- if we can't read the body, it implies
// that something has gone wrong with the connection, so we'll
// call that an error in calling ready.
return nil, fmt.Errorf("error reading body: %v", err)
}
return &EnvoyFetcherResponse{StatusCode: statusCode, Text: text}, nil
}
// SetReadyCheck will change the function we use to get check if Envoy is ready. This is
// here for testing; the assumption is that you'll call it at instantiation if you need
// to, then leave it alone.
func (w *EnvoyWatcher) SetReadyCheck(readyCheck envoyFetcher) {
w.readyCheck = readyCheck
}
// FetchEnvoyReady will check whether Envoy's ready endpoint is fetchable.
func (w *EnvoyWatcher) FetchEnvoyReady(ctx context.Context) {
succeeded := false
// Actually check if ready...
readyResponse, err := w.readyCheck(ctx)
// ...and see if we were able to.
if err == nil {
// Well, nothing blatantly failed, so check the status. (For the
// moment, we don't care about the text.)
if readyResponse.StatusCode == 200 {
succeeded = true
}
} else {
dlog.Debugf(ctx, "could not fetch Envoy status: %v", err)
}
w.mutex.Lock()
defer w.mutex.Unlock()
w.LastSucceeded = succeeded
}
// IsAlive returns true IFF Envoy should be considered alive.
func (w *EnvoyWatcher) IsAlive() bool {
w.mutex.Lock()
defer w.mutex.Unlock()
// Currently we just return LastSucceeded: we will not consider Envoy alive
// unless we were able to talk to it.
return w.LastSucceeded
}
// IsReady returns true IFF Envoy should be considered ready. Currently Envoy is
// considered ready whenever it's alive; this method is here for future-proofing.
func (w *EnvoyWatcher) IsReady() bool {
return w.IsAlive()
}