One V(1) line per GCP API call (insert/get/delete/aggregatedList) with outcome and operation name, V(2) request/per-instance detail, and raw googleapi status+reasons logged before classify collapses them. Curated fields only — cloud-init user-data never reaches logs (test-enforced). Co-Authored-By: Claude <noreply@anthropic.com>
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package gcp
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"slices"
|
|
|
|
"github.com/go-logr/logr"
|
|
"google.golang.org/api/googleapi"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
// classify maps a GCP API error onto the provider taxonomy:
|
|
// 404 → NotFound; 429 and quota-flavored 403s → QuotaExceeded;
|
|
// 400/401/other 403s → Permanent; 408/5xx and anything unrecognized
|
|
// (network errors, context cancellation) → Transient, because retrying is
|
|
// always safer than latching Failed on an error nobody taught this
|
|
// function to recognize.
|
|
func classify(err error) error {
|
|
var gerr *googleapi.Error
|
|
if !errors.As(err, &gerr) {
|
|
return provider.ErrTransient
|
|
}
|
|
switch {
|
|
case gerr.Code == http.StatusNotFound:
|
|
return provider.ErrNotFound
|
|
case gerr.Code == http.StatusTooManyRequests:
|
|
return provider.ErrQuotaExceeded
|
|
case gerr.Code == http.StatusForbidden && hasReason(gerr, "quotaExceeded", "rateLimitExceeded"):
|
|
return provider.ErrQuotaExceeded
|
|
case gerr.Code == http.StatusBadRequest,
|
|
gerr.Code == http.StatusUnauthorized,
|
|
gerr.Code == http.StatusForbidden:
|
|
return provider.ErrPermanent
|
|
default:
|
|
return provider.ErrTransient
|
|
}
|
|
}
|
|
|
|
func (p *Provider) wrapErr(op, id string, err error) error {
|
|
return provider.Wrap(classify(err), op, p.name, id, err)
|
|
}
|
|
|
|
// logAPIError records the raw googleapi error shape (HTTP status, reasons)
|
|
// at V(1) — classify collapses it onto the coarser provider taxonomy, so
|
|
// this line is the only place the original status survives.
|
|
func logAPIError(log logr.Logger, op string, err error) {
|
|
if !log.V(1).Enabled() {
|
|
return
|
|
}
|
|
kv := []any{"op", op, "error", err.Error()}
|
|
var gerr *googleapi.Error
|
|
if errors.As(err, &gerr) {
|
|
reasons := make([]string, 0, len(gerr.Errors))
|
|
for _, item := range gerr.Errors {
|
|
reasons = append(reasons, item.Reason)
|
|
}
|
|
kv = append(kv, "httpStatus", gerr.Code, "reasons", reasons)
|
|
}
|
|
log.V(1).Info("GCP API call failed", kv...)
|
|
}
|
|
|
|
func hasReason(gerr *googleapi.Error, reasons ...string) bool {
|
|
for _, item := range gerr.Errors {
|
|
if slices.Contains(reasons, item.Reason) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isAlreadyExists(err error) bool {
|
|
var gerr *googleapi.Error
|
|
return errors.As(err, &gerr) && gerr.Code == http.StatusConflict
|
|
}
|
|
|
|
func isNotFound(err error) bool {
|
|
var gerr *googleapi.Error
|
|
return errors.As(err, &gerr) && gerr.Code == http.StatusNotFound
|
|
}
|