|
1 | 1 | package config
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "context" |
4 | 5 | "fmt"
|
5 | 6 | "net/http"
|
6 | 7 | "net/url"
|
7 | 8 | "os"
|
8 | 9 | "path/filepath"
|
| 10 | + "sync" |
9 | 11 | "time"
|
10 | 12 |
|
11 | 13 | logger "github.com/sirupsen/logrus"
|
@@ -52,6 +54,7 @@ const (
|
52 | 54 | JiraConfigForAccessRequestsKey = "jira-config-for-access-requests"
|
53 | 55 | prodEnvNameDefaultValue = "production"
|
54 | 56 | JiraBaseURLDefaultValue = "https://issues.redhat.com"
|
| 57 | + proxyTestTimeout = 10 * time.Second |
55 | 58 | )
|
56 | 59 |
|
57 | 60 | var JiraConfigForAccessRequestsDefaultValue = AccessRequestsJiraConfiguration{
|
@@ -190,43 +193,115 @@ func GetBackplaneConfiguration() (bpConfig BackplaneConfiguration, err error) {
|
190 | 193 | return bpConfig, nil
|
191 | 194 | }
|
192 | 195 |
|
193 |
| -var clientDo = func(client *http.Client, req *http.Request) (*http.Response, error) { |
194 |
| - return client.Do(req) |
| 196 | +var testProxy = func(ctx context.Context, testURL string, proxyURL url.URL) error { |
| 197 | + // Try call the test URL via the proxy |
| 198 | + client := &http.Client{ |
| 199 | + Transport: &http.Transport{Proxy: http.ProxyURL(&proxyURL)}, |
| 200 | + } |
| 201 | + req, _ := http.NewRequestWithContext(ctx, "GET", testURL, nil) |
| 202 | + resp, err := client.Do(req) |
| 203 | + |
| 204 | + // Check the result |
| 205 | + if err != nil { |
| 206 | + return fmt.Errorf("proxy returned an error %v", err) |
| 207 | + } |
| 208 | + if resp.StatusCode != http.StatusOK { |
| 209 | + return fmt.Errorf("expected response code 200 but got %d", resp.StatusCode) |
| 210 | + } |
| 211 | + |
| 212 | + return nil |
195 | 213 | }
|
196 | 214 |
|
197 | 215 | func (config *BackplaneConfiguration) getFirstWorkingProxyURL(s []string) string {
|
198 |
| - bpURL := config.URL + "/healthz" |
| 216 | + if len(s) == 0 { |
| 217 | + logger.Debug("No proxy to use") |
| 218 | + return "" |
| 219 | + } |
199 | 220 |
|
200 |
| - client := &http.Client{ |
201 |
| - Timeout: 5 * time.Second, |
| 221 | + // If we only have one proxy, there is no need to waste time on tests, just use that one |
| 222 | + if len(s) == 1 { |
| 223 | + logger.Debug("Only one proxy to choose from, automatically using it") |
| 224 | + return s[0] |
202 | 225 | }
|
203 | 226 |
|
| 227 | + // Context to time out or cancel all tests once we are done |
| 228 | + ctx, cancel := context.WithTimeout(context.Background(), proxyTestTimeout) |
| 229 | + var wg sync.WaitGroup |
| 230 | + ch := make(chan *url.URL) |
| 231 | + |
| 232 | + bpURL := config.URL + "/healthz" |
| 233 | + |
| 234 | + failures := 0 |
204 | 235 | for _, p := range s {
|
| 236 | + // Parse the proxy URL |
205 | 237 | proxyURL, err := url.ParseRequestURI(p)
|
206 | 238 | if err != nil {
|
207 | 239 | logger.Debugf("proxy-url: '%v' could not be parsed.", p)
|
| 240 | + failures++ |
208 | 241 | continue
|
209 | 242 | }
|
210 | 243 |
|
211 |
| - client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} |
212 |
| - req, _ := http.NewRequest("GET", bpURL, nil) |
213 |
| - resp, err := clientDo(client, req) |
214 |
| - if err != nil { |
215 |
| - logger.Infof("Proxy: %s returned an error: %s", proxyURL, err) |
216 |
| - continue |
217 |
| - } |
218 |
| - if resp.StatusCode == http.StatusOK { |
219 |
| - return p |
220 |
| - } |
221 |
| - logger.Infof("proxy: %s did not pass healthcheck, expected response code 200, got %d, discarding", p, resp.StatusCode) |
| 244 | + wg.Add(1) |
| 245 | + go func(proxyURL url.URL) { |
| 246 | + defer wg.Done() |
| 247 | + |
| 248 | + // Do the proxy test |
| 249 | + proxyErr := testProxy(ctx, bpURL, proxyURL) |
| 250 | + if proxyErr != nil { |
| 251 | + logger.Infof("Discarding proxy %s due to error: %s", proxyURL.String(), proxyErr) |
| 252 | + ch <- nil |
| 253 | + return |
| 254 | + } |
| 255 | + |
| 256 | + // This test succeeded, send to the main thread |
| 257 | + ch <- &proxyURL |
| 258 | + }(*proxyURL) |
222 | 259 | }
|
223 | 260 |
|
224 |
| - if len(s) > 0 { |
225 |
| - logger.Infof("falling back to first proxy-url after all proxies failed health checks: %s", s[0]) |
226 |
| - return s[0] |
| 261 | + // Default to the first |
| 262 | + chosenURL := s[0] |
| 263 | + |
| 264 | + // Loop until all tests have failed or we get a single success |
| 265 | +loop: |
| 266 | + for failures < len(s) { |
| 267 | + select { |
| 268 | + case proxyURL := <-ch: // A proxy returned a result |
| 269 | + // nil means the test failed |
| 270 | + if proxyURL == nil { |
| 271 | + failures++ |
| 272 | + continue |
| 273 | + } |
| 274 | + |
| 275 | + // This proxy passed |
| 276 | + chosenURL = proxyURL.String() |
| 277 | + logger.Infof("proxy that responded first was %s", chosenURL) |
| 278 | + |
| 279 | + break loop |
| 280 | + |
| 281 | + case <-ctx.Done(): // We timed out waiting for a proxy to pass |
| 282 | + logger.Warnf("falling back to first proxy-url after all proxies timed out: %s", s[0]) |
| 283 | + |
| 284 | + break loop |
| 285 | + } |
227 | 286 | }
|
228 | 287 |
|
229 |
| - return "" |
| 288 | + // Cancel any remaining requests |
| 289 | + cancel() |
| 290 | + |
| 291 | + // Ignore any other valid proxies, until the channel is closed |
| 292 | + go func() { |
| 293 | + for lateProxy := range ch { |
| 294 | + if lateProxy != nil { |
| 295 | + logger.Infof("proxy %s responded too late", lateProxy) |
| 296 | + } |
| 297 | + } |
| 298 | + }() |
| 299 | + |
| 300 | + // Wait for goroutines to end, then close the channel |
| 301 | + wg.Wait() |
| 302 | + close(ch) |
| 303 | + |
| 304 | + return chosenURL |
230 | 305 | }
|
231 | 306 |
|
232 | 307 | func validateConfig() error {
|
|
0 commit comments