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