| package wrapngo | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"errors" | 
 | 	"fmt" | 
 | 	"net/http" | 
 | 	"time" | 
 |  | 
 | 	"github.com/cenkalti/backoff/v4" | 
 | 	"github.com/packethost/packngo" | 
 | 	"k8s.io/klog/v2" | 
 | ) | 
 |  | 
 | // wrap a given fn in some reliability-increasing duct tape: context support and | 
 | // exponential backoff retries for intermittent connectivity issues. This allows | 
 | // us to use packngo code instead of writing our own API stub for Equinix Metal. | 
 | // | 
 | // The given fn will be retried until it returns a 'permanent' Equinix error (see | 
 | // isPermanentEquinixError) or the given context expires. Additionally, fn will | 
 | // be called with a brand new packngo client tied to the context of the wrap | 
 | // call. Finally, the given client will also have some logging middleware | 
 | // attached to it which can be activated by setting verbosity 5 (or greater) on | 
 | // this file. | 
 | // | 
 | // The wrapped fn can be either just a plain packngo method or some complicated | 
 | // idempotent logic, as long as it cooperates with the above contract. | 
 | func wrap[U any](ctx context.Context, cl *client, fn func(*packngo.Client) (U, error)) (U, error) { | 
 | 	var zero U | 
 | 	if err := cl.serializer.up(ctx); err != nil { | 
 | 		return zero, err | 
 | 	} | 
 | 	defer cl.serializer.down() | 
 |  | 
 | 	bc := backoff.WithContext(cl.o.BackOff(), ctx) | 
 | 	pngo, err := cl.clientForContext(ctx) | 
 | 	if err != nil { | 
 | 		// Generally this shouldn't happen other than with programming errors, so we | 
 | 		// don't back this off. | 
 | 		return zero, fmt.Errorf("could not crate equinix client: %w", err) | 
 | 	} | 
 |  | 
 | 	var res U | 
 | 	err = backoff.Retry(func() error { | 
 | 		res, err = fn(pngo) | 
 | 		if isPermanentEquinixError(err) { | 
 | 			return backoff.Permanent(err) | 
 | 		} | 
 | 		return err | 
 | 	}, bc) | 
 | 	if err != nil { | 
 | 		return zero, err | 
 | 	} | 
 | 	return res, nil | 
 | } | 
 |  | 
 | type injectContextRoundTripper struct { | 
 | 	ctx      context.Context | 
 | 	original http.RoundTripper | 
 | 	metrics  *metricsSet | 
 | } | 
 |  | 
 | func (r *injectContextRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | 
 | 	klog.V(5).Infof("Request -> %v", req.URL.String()) | 
 | 	start := time.Now() | 
 | 	res, err := r.original.RoundTrip(req.WithContext(r.ctx)) | 
 | 	latency := time.Since(start) | 
 | 	r.metrics.onAPIRequestDone(req, res, err, latency) | 
 |  | 
 | 	if err != nil { | 
 | 		klog.V(5).Infof("HTTP error <- %v", err) | 
 | 	} else { | 
 | 		klog.V(5).Infof("Response <- %v", res.Status) | 
 | 	} | 
 | 	return res, err | 
 | } | 
 |  | 
 | func (c *client) clientForContext(ctx context.Context) (*packngo.Client, error) { | 
 | 	httpcl := &http.Client{ | 
 | 		Transport: &injectContextRoundTripper{ | 
 | 			ctx:      ctx, | 
 | 			original: http.DefaultTransport, | 
 | 			metrics:  c.metrics, | 
 | 		}, | 
 | 	} | 
 | 	return packngo.NewClient(packngo.WithAuth(c.username, c.token), packngo.WithHTTPClient(httpcl)) | 
 | } | 
 |  | 
 | // httpStatusCode extracts the status code from error values returned by | 
 | // packngo methods. | 
 | func httpStatusCode(err error) int { | 
 | 	var er *packngo.ErrorResponse | 
 | 	if err != nil && errors.As(err, &er) { | 
 | 		return er.Response.StatusCode | 
 | 	} | 
 | 	return -1 | 
 | } | 
 |  | 
 | // IsNotFound returns true if the given error is an Equinix packngo/wrapngo 'not | 
 | // found' error. | 
 | func IsNotFound(err error) bool { | 
 | 	return httpStatusCode(err) == http.StatusNotFound | 
 | } | 
 |  | 
 | func isPermanentEquinixError(err error) bool { | 
 | 	// Invalid argument/state errors from wrapping. | 
 | 	if errors.Is(err, ErrRaceLost) { | 
 | 		return true | 
 | 	} | 
 | 	if errors.Is(err, ErrNoReservationProvided) { | 
 | 		return true | 
 | 	} | 
 | 	// Real errors returned from equinix. | 
 | 	st := httpStatusCode(err) | 
 | 	switch st { | 
 | 	case http.StatusUnauthorized: | 
 | 		return true | 
 | 	case http.StatusForbidden: | 
 | 		return true | 
 | 	case http.StatusNotFound: | 
 | 		return true | 
 | 	case http.StatusUnprocessableEntity: | 
 | 		return true | 
 | 	} | 
 | 	return false | 
 | } |