mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
package awsmodel
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const ec2ErrorCatalogPath = "models/ec2/error-codes.json"
|
|
|
|
type ec2ErrorClass string
|
|
|
|
const (
|
|
ec2ErrorClassClient ec2ErrorClass = "client"
|
|
ec2ErrorClassServer ec2ErrorClass = "server"
|
|
)
|
|
|
|
type ec2ErrorCatalogFile struct {
|
|
Source string `json:"source"`
|
|
VerifiedOn string `json:"verifiedOn"`
|
|
CommonClient []string `json:"commonClient"`
|
|
SpecificClient []string `json:"specificClient"`
|
|
Server []string `json:"server"`
|
|
}
|
|
|
|
type ec2ErrorCatalog struct {
|
|
metadata ec2ErrorCatalogFile
|
|
codes map[string]ec2ErrorClass
|
|
}
|
|
|
|
var loadEC2ErrorCatalog = sync.OnceValues(func() (*ec2ErrorCatalog, error) {
|
|
contents, err := modelFS.ReadFile(ec2ErrorCatalogPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("awsmodel: read EC2 error catalog: %w", err)
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(contents))
|
|
decoder.DisallowUnknownFields()
|
|
var file ec2ErrorCatalogFile
|
|
if err := decoder.Decode(&file); err != nil {
|
|
return nil, fmt.Errorf("awsmodel: parse EC2 error catalog: %w", err)
|
|
}
|
|
if err := decoder.Decode(new(any)); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
err = errors.New("multiple JSON values")
|
|
}
|
|
return nil, fmt.Errorf("awsmodel: parse EC2 error catalog: %w", err)
|
|
}
|
|
if file.Source == "" || file.VerifiedOn == "" {
|
|
return nil, errors.New("awsmodel: EC2 error catalog requires source and verifiedOn")
|
|
}
|
|
|
|
catalog := &ec2ErrorCatalog{metadata: file, codes: make(map[string]ec2ErrorClass)}
|
|
add := func(label string, values []string, class ec2ErrorClass) error {
|
|
if !slices.IsSorted(values) {
|
|
return fmt.Errorf("awsmodel: EC2 error catalog %s codes are not sorted", label)
|
|
}
|
|
for _, code := range values {
|
|
if code == "" {
|
|
return fmt.Errorf("awsmodel: EC2 error catalog %s contains an empty code", label)
|
|
}
|
|
if previous, exists := catalog.codes[code]; exists {
|
|
return fmt.Errorf("awsmodel: EC2 error catalog code %q is duplicated (%s and %s)", code, previous, label)
|
|
}
|
|
catalog.codes[code] = class
|
|
}
|
|
return nil
|
|
}
|
|
if err := add("commonClient", file.CommonClient, ec2ErrorClassClient); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := add("specificClient", file.SpecificClient, ec2ErrorClassClient); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := add("server", file.Server, ec2ErrorClassServer); err != nil {
|
|
return nil, err
|
|
}
|
|
return catalog, nil
|
|
})
|
|
|
|
// ValidateEC2ErrorResponse validates an EC2 Query API error envelope against
|
|
// the checked-in catalog curated from AWS's EC2 error-code reference. The AWS
|
|
// EC2 model declares no operation errors, so this is intentionally a separate
|
|
// oracle.
|
|
func ValidateEC2ErrorResponse(status int, body []byte) ([]Violation, error) {
|
|
root, err := parseXML(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("awsmodel: decode EC2 error response: %w", err)
|
|
}
|
|
if root.name != "Response" {
|
|
return nil, fmt.Errorf("awsmodel: decode EC2 error response: root element is %q, want %q", root.name, "Response")
|
|
}
|
|
errorsNode := directXMLChild(root, "Errors")
|
|
if errorsNode == nil {
|
|
return nil, errors.New("awsmodel: decode EC2 error response: Errors element is missing")
|
|
}
|
|
errorNode := directXMLChild(errorsNode, "Error")
|
|
if errorNode == nil {
|
|
return nil, errors.New("awsmodel: decode EC2 error response: Error element is missing")
|
|
}
|
|
code := directXMLText(errorNode, "Code")
|
|
if code == "" {
|
|
return nil, errors.New("awsmodel: decode EC2 error response: Code element is missing")
|
|
}
|
|
|
|
catalog, err := loadEC2ErrorCatalog()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
class, known := catalog.codes[code]
|
|
if !known {
|
|
return []Violation{{
|
|
Rule: RuleErrorCode,
|
|
Path: "$error.Code",
|
|
Message: fmt.Sprintf("error code %q is not in the curated EC2 catalog", code),
|
|
}}, nil
|
|
}
|
|
|
|
wantStatusClass := 4
|
|
if class == ec2ErrorClassServer {
|
|
wantStatusClass = 5
|
|
}
|
|
if status/100 != wantStatusClass {
|
|
return []Violation{{
|
|
Rule: RuleHTTPStatus,
|
|
Path: "$response.status",
|
|
Message: fmt.Sprintf("status %d is not in the %dxx class required for EC2 %s error %q", status, wantStatusClass, class, code),
|
|
}}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func directXMLChild(node *xmlNode, name string) *xmlNode {
|
|
for _, child := range node.children {
|
|
if child.name == name {
|
|
return child
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func directXMLText(node *xmlNode, name string) string {
|
|
child := directXMLChild(node, name)
|
|
if child == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(child.text.String())
|
|
}
|