awsjson

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package awsjson is billet's client for AWS services that speak JSON 1.1.

EXTRACTED FROM internal/provider/codebuild RATHER THAN WRITTEN AGAIN, for the reason internal/awscreds was: a second package needed the same machinery, and a compute backend cannot be a library for the rest of billet. The server role now reaches Parameter Store for the deployment's identity material, and copying four hundred lines of retry ladder, redirect refusal, error classification and endpoint derivation would be the two-pins problem in the code that signs requests against somebody else's API.

WHAT IT DOES NOT OWN is what each service means. An action's request and response shapes, which errors are verdicts, and which calls may be retried at all belong to the caller; this is the transport, the signature, and the classification of outcomes that are the same for every service.

IT IS A LEAF. It imports billet's signer, its credential chain and the config package below both, and nothing else, so a provider and the control plane can both use it without either importing the other. config is there for one rule: which DNS suffix a region's partition uses. That question is asked on both sides of the wire — here to build a host, and in config to refuse a spot queue URL that names the other partition's — and config may import nothing of billet's, so the rule is declared there and called from here rather than written twice.

Index

Constants

View Source
const APITimeout = 30 * time.Second

APITimeout bounds one API call. Generous next to the calls themselves, because what is being bounded is a stall rather than the work.

View Source
const ContentType = "application/x-amz-json-1.1"

ContentType is what AWS JSON 1.1 requires. It is SIGNED, so a mismatch between the header sent and the header signed is a 403 naming nothing.

View Source
const MaxAttempts = 3

MaxAttempts bounds how many times one call is issued.

THREE, the same as the ec2 client and for the same reason: a caller is often a launch path a job is waiting on, and a long retry ladder turns a throttled region into a launch that outlives the plane's command timeout.

Variables

View Source
var ErrRedirected = errors.New("awsjson: the api endpoint answered with a redirect")

ErrRedirected is billet's own refusal of a redirect from a signed endpoint.

A SENTINEL RATHER THAN PROSE, for the reason the ec2 client documents: net/http wraps whatever CheckRedirect returns in a *url.Error, and THAT type renders the whole redirect target — including a query string chosen by whatever answered. The call boundary recognises this and replaces the wrapper rather than wrapping it.

ONE SENTINEL FOR EVERY SERVICE, because errors.Is has to match across the package boundary: a per-client value would make every caller's own check false.

Functions

func CodeOf

func CodeOf(err error) (string, bool)

CodeOf reports the API error code in a chain, and whether there was one.

func DNSSuffixFor

func DNSSuffixFor(region string) string

DNSSuffixFor is the partition's DNS suffix for a region.

THE RULE ITSELF LIVES IN internal/config, and this is one call rather than a second copy. config is the leaf everything reads and may import nothing of billet's, so its SQS host validator cannot ask this package the question — and it has to select exactly this suffix or it admits a queue host in the other partition. Keeping the name here means nothing above changes. config.TapPrefix is the same arrangement for the firecracker provider, for the same reason.

func EndpointFor

func EndpointFor(service, region string) string

EndpointFor derives the regional endpoint for one service.

THE SUFFIX IS NOT THE SAME IN EVERY PARTITION, the rule the ec2 client already states: a region check deliberately admits partitions billet has never run in, so the commercial suffix would derive a host that does not exist for `cn-north-1`. AWS China is reached at amazonaws.com.cn; GovCloud uses the commercial suffix.

func ParseAPIError

func ParseAPIError(service string, payload []byte, status int) error

ParseAPIError turns a non-200 into an APIError, keeping the status when the body is not the shape it should be.

A GATEWAY, A PROXY OR A LOAD BALANCER can answer instead of AWS, and its body is not this shape. The status is all there is, and it is enough to decide whether to retry.

func Retryable

func Retryable(err error) bool

Retryable reports whether an attempt is worth repeating.

A THROTTLE AND A 5XX ARE "NOT NOW"; EVERYTHING ELSE IS "NO". Retrying a rejected parameter spends the caller's deadline arriving at the same answer.

func RetryableRefusal

func RetryableRefusal(err error) bool

RetryableRefusal is Retryable narrowed to outcomes AWS ANSWERED WITH.

The distinction is whether billet knows the request was not acted on. A throttle says so; a dropped connection, a body that would not parse and a 5xx do not — and for an action that creates something, "may have been processed" and "was processed" have to be treated the same way.

func StatusOf

func StatusOf(err error) (int, bool)

StatusOf reports the HTTP status in a chain, and whether there was one.

func Unqualified

func Unqualified(t string) string

Unqualified strips the namespace AWS prefixes onto a JSON error type.

`__type` arrives either bare (`ResourceNotFoundException`) or fully qualified (`com.amazonaws.codebuild#ResourceNotFoundException`), and both forms are the same verdict. Matching the raw string would make every branch depend on which form a given endpoint happens to send.

Types

type APIError

type APIError struct {
	// Service names the caller in the message, so an operator reading a log sees
	// which of billet's clients was refused rather than a bare code.
	Service string
	Code    string
	Message string
	Status  int
}

APIError is a refusal one of the JSON APIs described.

THE TYPE IS THE PART THAT MATTERS and it is kept separate from the message, because callers branch on it: a build that is already complete is success for an idempotent teardown, and telling that from a real failure by matching prose is how a teardown failure gets swallowed. The wire's `__type` is often qualified (`com.amazonaws...#ResourceNotFoundException`), so Code strips the qualifier.

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {

	// Now is time.Now, replaceable so a test can pin a signature.
	Now func() time.Time
	// Sleep waits between attempts, replaceable so a test does not.
	Sleep func(ctx context.Context, d time.Duration) error
	// contains filtered or unexported fields
}

Client talks a JSON 1.1 API over signed HTTPS.

func New

func New(name, region string, creds awscreds.Source) *Client

New builds a signed client.

THERE IS NO DEFAULT CREDENTIAL SOURCE. A caller passes one rather than having this reach for the ambient chain, so nothing can end up signing with credentials it did not choose.

func (*Client) CredentialSource

func (c *Client) CredentialSource() awscreds.Source

CredentialSource is where this client resolves credentials, or nil.

EXPOSED SO A CONSTRUCTOR CAN REFUSE A MISSING OR TYPED-NIL SOURCE, which is the one thing a caller has to check before the first signed call: a typed nil satisfies the interface, passes a plain `== nil`, and dereferences on a path that is already holding leases.

func (Client) Format

func (c Client) Format(f fmt.State, _ rune)

Format catches every verb. Implementing it means fmt never consults String or GoString, which is why they are also called directly by the redaction test.

func (Client) GoString

func (c Client) GoString() string

GoString covers %#v.

func (*Client) HTTPClient

func (c *Client) HTTPClient() *http.Client

HTTPClient exposes the transport so a caller can install a redirect refusal or a bounded timeout of its own.

func (*Client) Invoke

func (c *Client) Invoke(
	ctx context.Context, endpoint, service, target, action string, in, out any,
	again func(error) bool,
) error

Invoke issues one action against one endpoint and unmarshals the response.

`again` DECIDES WHAT MAY BE REPEATED, and it is the caller's rather than this package's: a read may be retried on anything transient, while an action that CREATES something may be retried only on an outcome AWS itself refused to act on. Passing the wrong one is how a launch becomes two.

func (Client) LogValue

func (c Client) LogValue() slog.Value

LogValue is what slog consults; its JSON handler ignores fmt entirely.

func (Client) MarshalJSON

func (c Client) MarshalJSON() ([]byte, error)

MarshalJSON keeps a client out of anything that serializes it structurally.

func (*Client) Region

func (c *Client) Region() string

Region is the signing region this client was built for.

func (*Client) SetCredentials

func (c *Client) SetCredentials(src awscreds.Source)

SetCredentials replaces the credential source.

func (*Client) SetHTTPClient

func (c *Client) SetHTTPClient(h *http.Client)

SetHTTPClient replaces the transport. A caller that supplies one owns its timeout and its redirect policy.

func (Client) String

func (c Client) String() string

REDACTED, BECAUSE IT HOLDS A CREDENTIAL SOURCE IN AN UNEXPORTED FIELD.

fmt cannot invoke methods through an unexported field — reflect refuses — so a source's own redaction is never consulted when the struct AROUND it is printed structurally. On the ec2 side `%+v` on a client holding a value-typed source printed the secret access key in full, past three layers of redaction that all worked in isolation.

ON A VALUE RECEIVER, which is the rule and which the ec2 client broke on its first attempt: a pointer receiver is not consulted when a VALUE is formatted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL