blob: d5dab7c6f313c79caa862d7f25623fed004898cb [file] [log] [blame]
Mateusz Zalega6a058e72022-11-30 18:03:07 +01001package wrapngo
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
Serge Bazanskidea7cd02023-04-26 13:58:17 +02008 "time"
Mateusz Zalega6a058e72022-11-30 18:03:07 +01009
10 "github.com/cenkalti/backoff/v4"
11 "github.com/packethost/packngo"
12 "k8s.io/klog/v2"
13)
14
15// wrap a given fn in some reliability-increasing duct tape: context support and
16// exponential backoff retries for intermittent connectivity issues. This allows
17// us to use packngo code instead of writing our own API stub for Equinix Metal.
18//
19// The given fn will be retried until it returns a 'permanent' Equinix error (see
20// isPermanentEquinixError) or the given context expires. Additionally, fn will
21// be called with a brand new packngo client tied to the context of the wrap
22// call. Finally, the given client will also have some logging middleware
23// attached to it which can be activated by setting verbosity 5 (or greater) on
24// this file.
25//
26// The wrapped fn can be either just a plain packngo method or some complicated
27// idempotent logic, as long as it cooperates with the above contract.
28func wrap[U any](ctx context.Context, cl *client, fn func(*packngo.Client) (U, error)) (U, error) {
29 var zero U
Serge Bazanskidea7cd02023-04-26 13:58:17 +020030 if err := cl.serializer.up(ctx); err != nil {
31 return zero, err
Mateusz Zalega6a058e72022-11-30 18:03:07 +010032 }
Serge Bazanskidea7cd02023-04-26 13:58:17 +020033 defer cl.serializer.down()
Mateusz Zalega6a058e72022-11-30 18:03:07 +010034
35 bc := backoff.WithContext(cl.o.BackOff(), ctx)
36 pngo, err := cl.clientForContext(ctx)
37 if err != nil {
38 // Generally this shouldn't happen other than with programming errors, so we
39 // don't back this off.
40 return zero, fmt.Errorf("could not crate equinix client: %w", err)
41 }
42
43 var res U
44 err = backoff.Retry(func() error {
45 res, err = fn(pngo)
46 if isPermanentEquinixError(err) {
47 return backoff.Permanent(err)
48 }
49 return err
50 }, bc)
51 if err != nil {
52 return zero, err
53 }
54 return res, nil
55}
56
57type injectContextRoundTripper struct {
58 ctx context.Context
59 original http.RoundTripper
Serge Bazanskidea7cd02023-04-26 13:58:17 +020060 metrics *metricsSet
Mateusz Zalega6a058e72022-11-30 18:03:07 +010061}
62
63func (r *injectContextRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
64 klog.V(5).Infof("Request -> %v", req.URL.String())
Serge Bazanskidea7cd02023-04-26 13:58:17 +020065 start := time.Now()
Mateusz Zalega6a058e72022-11-30 18:03:07 +010066 res, err := r.original.RoundTrip(req.WithContext(r.ctx))
Serge Bazanskidea7cd02023-04-26 13:58:17 +020067 latency := time.Since(start)
68 r.metrics.onAPIRequestDone(req, res, err, latency)
69
Serge Bazanskib78919e2023-04-17 15:51:58 +020070 if err != nil {
71 klog.V(5).Infof("HTTP error <- %v", err)
72 } else {
73 klog.V(5).Infof("Response <- %v", res.Status)
74 }
Mateusz Zalega6a058e72022-11-30 18:03:07 +010075 return res, err
76}
77
78func (c *client) clientForContext(ctx context.Context) (*packngo.Client, error) {
79 httpcl := &http.Client{
80 Transport: &injectContextRoundTripper{
81 ctx: ctx,
82 original: http.DefaultTransport,
Serge Bazanskidea7cd02023-04-26 13:58:17 +020083 metrics: c.metrics,
Mateusz Zalega6a058e72022-11-30 18:03:07 +010084 },
85 }
86 return packngo.NewClient(packngo.WithAuth(c.username, c.token), packngo.WithHTTPClient(httpcl))
87}
88
89// httpStatusCode extracts the status code from error values returned by
90// packngo methods.
91func httpStatusCode(err error) int {
92 var er *packngo.ErrorResponse
93 if err != nil && errors.As(err, &er) {
94 return er.Response.StatusCode
95 }
96 return -1
97}
98
99// IsNotFound returns true if the given error is an Equinix packngo/wrapngo 'not
100// found' error.
101func IsNotFound(err error) bool {
102 return httpStatusCode(err) == http.StatusNotFound
103}
104
105func isPermanentEquinixError(err error) bool {
106 // Invalid argument/state errors from wrapping.
107 if errors.Is(err, ErrRaceLost) {
108 return true
109 }
110 if errors.Is(err, ErrNoReservationProvided) {
111 return true
112 }
113 // Real errors returned from equinix.
114 st := httpStatusCode(err)
115 switch st {
116 case http.StatusUnauthorized:
117 return true
118 case http.StatusForbidden:
119 return true
120 case http.StatusNotFound:
121 return true
122 case http.StatusUnprocessableEntity:
123 return true
124 }
125 return false
126}