mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
167 lines
5.1 KiB
Go
167 lines
5.1 KiB
Go
package awsmodel
|
|
|
|
import (
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// Verdict is the outcome of one generated request.
|
|
type Verdict string
|
|
|
|
const (
|
|
VerdictPass Verdict = "pass"
|
|
VerdictFinding Verdict = "finding"
|
|
VerdictInconclusive Verdict = "inconclusive"
|
|
// VerdictUndeclaredError is a rejection refused with a validation code
|
|
// that neither the operation nor the common errors declare.
|
|
VerdictUndeclaredError Verdict = "undeclared_error"
|
|
)
|
|
|
|
// validationErrorCodes are the codes that refuse a request as failing
|
|
// parameter validation, as opposed to naming something missing or conflicting
|
|
// with state. InvalidParameterValueException is not modelled but is one.
|
|
var validationErrorCodes = map[string]bool{
|
|
"InvalidInput": true,
|
|
"InvalidParameter": true,
|
|
"InvalidParameterCombination": true,
|
|
"InvalidParameterException": true,
|
|
"InvalidParameterValue": true,
|
|
"InvalidParameterValueException": true,
|
|
"MissingParameter": true,
|
|
"SerializationException": true,
|
|
"UnknownParameter": true,
|
|
"ValidationError": true,
|
|
"ValidationException": true,
|
|
}
|
|
|
|
// commonValidationCodes are validation errors AWS lists as common to every
|
|
// service rather than declaring them on each operation.
|
|
var commonValidationCodes = map[string]bool{
|
|
"InvalidParameterCombination": true,
|
|
"InvalidParameterValue": true,
|
|
"MissingParameter": true,
|
|
"ValidationError": true,
|
|
"ValidationException": true,
|
|
}
|
|
|
|
// IsValidationErrorCode reports whether code is a parameter validation error,
|
|
// including EC2's per-resource "*.Malformed" codes.
|
|
func IsValidationErrorCode(code string) bool {
|
|
return validationErrorCodes[code] || strings.HasSuffix(code, ".Malformed")
|
|
}
|
|
|
|
// RequestResult is what came back for one generated request. Code is empty for
|
|
// a success.
|
|
type RequestResult struct {
|
|
Status int
|
|
Code string
|
|
Message string
|
|
}
|
|
|
|
func (r RequestResult) succeeded() bool { return r.Status >= 200 && r.Status < 300 }
|
|
|
|
// conditionalRequirementCodes name a member the request omits or a pairing it
|
|
// breaks. Models mark conditionally required members optional, so an
|
|
// acceptance request refused with one of these may be refused by AWS too.
|
|
var conditionalRequirementCodes = map[string]bool{
|
|
"InvalidParameterCombination": true,
|
|
"MissingParameter": true,
|
|
}
|
|
|
|
// RequestJudge judges a plan's cases in plan order. A refusal counts for a
|
|
// rejection case only if the acceptance cases it builds on were accepted; a
|
|
// rejection case that succeeds is a finding regardless.
|
|
type RequestJudge struct {
|
|
declared []string
|
|
refused map[string]bool
|
|
}
|
|
|
|
func (p RequestPlan) NewJudge() *RequestJudge {
|
|
return &RequestJudge{declared: p.DeclaredErrors, refused: map[string]bool{}}
|
|
}
|
|
|
|
func (j *RequestJudge) Judge(c RequestCase, result RequestResult) Verdict {
|
|
validation := !result.succeeded() && IsValidationErrorCode(result.Code)
|
|
if c.Acceptance() {
|
|
baseRefused := c.Member != "" && j.refused[""]
|
|
j.refused[c.Member] = validation
|
|
switch {
|
|
case baseRefused:
|
|
return VerdictInconclusive
|
|
case result.succeeded():
|
|
return VerdictPass
|
|
case validation && !conditionalRequirementCodes[result.Code]:
|
|
return VerdictFinding
|
|
default:
|
|
return VerdictInconclusive
|
|
}
|
|
}
|
|
if result.succeeded() {
|
|
return VerdictFinding
|
|
}
|
|
// A member with no acceptance case, such as a pagination token, gives a
|
|
// refusal nothing to be attributed against.
|
|
baseRefused, baseJudged := j.refused[""]
|
|
memberRefused, memberJudged := j.refused[c.Member]
|
|
switch {
|
|
case !validation, !baseJudged, !memberJudged, baseRefused, memberRefused:
|
|
return VerdictInconclusive
|
|
case len(j.declared) > 0 && !slices.Contains(j.declared, result.Code) && !commonValidationCodes[result.Code]:
|
|
return VerdictUndeclaredError
|
|
default:
|
|
return VerdictPass
|
|
}
|
|
}
|
|
|
|
// DecodeRequestResult extracts the error code and message from a response.
|
|
func DecodeRequestResult(service Service, status int, header http.Header, body []byte) RequestResult {
|
|
result := RequestResult{Status: status}
|
|
if status >= 200 && status < 300 {
|
|
return result
|
|
}
|
|
model, err := Load(service)
|
|
if err != nil {
|
|
return result
|
|
}
|
|
// rest-json services may carry the code only in a header.
|
|
if code := header.Get("X-Amzn-Errortype"); code != "" {
|
|
result.Code = trimErrorCode(code)
|
|
} else if code, err := model.decodeErrorCode(body); err == nil {
|
|
result.Code = code
|
|
}
|
|
result.Message = decodeErrorMessage(model.metadata.Protocol, body)
|
|
return result
|
|
}
|
|
|
|
func trimErrorCode(code string) string {
|
|
code, _, _ = strings.Cut(code, ":")
|
|
if separator := strings.LastIndex(code, "#"); separator >= 0 {
|
|
code = code[separator+1:]
|
|
}
|
|
return code
|
|
}
|
|
|
|
func decodeErrorMessage(protocol string, body []byte) string {
|
|
switch protocol {
|
|
case "json", "rest-json":
|
|
document, err := decodeJSONResponse(body)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
fields, _ := document.(map[string]any)
|
|
for _, key := range []string{"message", "Message", "errorMessage"} {
|
|
if message, ok := fields[key].(string); ok {
|
|
return message
|
|
}
|
|
}
|
|
return ""
|
|
default:
|
|
root, err := parseXML(body)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return findXMLText(root, "Message")
|
|
}
|
|
}
|