Documentation
¶
Overview ¶
Package transport sends every request made against the platform. It owns the three things a caller should not have to: which token an operation takes, which address it goes to, and how a refusal reads.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnauthenticated means no usable credentials. The identity was not // established, so the request was not considered. ErrUnauthenticated = errors.New("not authenticated") // ErrTokenExpired is the case worth retrying: the credential was valid and // is not any more, and a fresh one fixes it. It also unwraps to // ErrUnauthenticated. ErrTokenExpired = errors.New("token expired") // ErrPermissionDenied means the identity is established and not allowed. // Retrying with a fresh token cannot help. ErrPermissionDenied = errors.New("not permitted") ErrNotFound = errors.New("not found") // ErrInvalidArgument means the request was understood and refused as // malformed. Sending it again unchanged will be refused again. ErrInvalidArgument = errors.New("invalid argument") // ErrConflict means the request contradicts the current state — creating // something that exists, deleting something in use. ErrConflict = errors.New("conflict") ErrRateLimited = errors.New("rate limited") // other case where trying again later is reasonable. ErrUnavailable = errors.New("service unavailable") )
The kinds a refusal falls into.
A caller has to be able to tell "get another token" from "you may not do that" from "try again later", because the three call for opposite responses and only one of them is worth retrying. Without these it has to match on APIError.Code, which means every caller carries its own copy of the mapping below — and the copies drift the day the platform adds a code.
They are reached with errors.Is. An APIError unwraps to the kind it belongs to, and to more than one where the finer answer is also useful:
errors.Is(err, transport.ErrTokenExpired) // mint a new one and retry errors.Is(err, transport.ErrUnauthenticated) // ... or just: not signed in
var ErrNoServiceAddress = errors.New("no address for service")
var ErrRequestFailed = fmt.Errorf("request failed: %w", ErrUnavailable)
ErrRequestFailed is a request that never got an answer — the connection failed, the reply was not JSON. It unwraps to ErrUnavailable, because to a caller deciding what to do next it is the same situation as a 503.
Functions ¶
func RewriteDomain ¶
RewriteDomain swaps everything after the first label of the host, so compute.leaflow.cloud becomes compute.leaflow.test while keeping the scheme and the port a local stack needs.
Types ¶
type APIError ¶
APIError is a refusal from a service. Callers branch on Code rather than matching prose: the wording changes, the code is contract.
func (*APIError) Unwrap ¶
Unwrap places this refusal among the kinds above.
A slice rather than a single error, so that a caller can ask the precise question or the general one and get a true answer to both: an expired token is an expired token and is also a failure to authenticate.
The service's code is preferred over the status because it is the part that is contract — the status is chosen by whichever component answered, and the gateway and IAM do not always agree on it.
type Addresses ¶
Addresses says where a service answers.
It is given the address the contract declares and returns the one to use, so that pointing at another deployment is a decision made in one place rather than a string edited in several.
type Credentials ¶
type Credentials interface {
Token(ctx context.Context, kind spec.Credential) (string, error)
// Invalidate drops a cached token the service has just refused, so that the
// one retry fetches a fresh one. An error means the retry cannot help and is
// not attempted — which is the honest answer when the token was handed in
// from outside and there is no other one to get.
Invalidate(kind spec.Credential) error
}
Credentials supplies the token an operation takes.
An interface, because the two callers want opposite things from one. A command line serves one person on one machine: it keeps a refresh token in the keychain, renews it, and exchanges it for a project token, and "the current user" is a thing it can meaningfully have. A service serves many people at once and is handed a token per request; it must not touch a keychain, and there is no current user for it to ask about.
Everything below this line is the same either way — the retry, the error codes, the addresses — so the difference is confined to here.
type Endpoints ¶
type Endpoints struct {
// Domain rewrites the domain part of every address a contract declares, so
// that compute.leaflow.cloud becomes compute.leaflow.test for a self-hosted
// deployment. It is a rewrite of a stated address, not a way of deriving one.
Domain string
// Overrides replaces one service's address outright, for a local stack whose
// addresses do not follow from a domain — different ports, hostnames that are
// nobody's subdomain.
Overrides map[string]string
}
Endpoints resolves a service's address for one deployment.
Zero value means "use the contracts as written", which is what talking to the hosted platform needs and therefore what a caller who says nothing gets.
Nothing is derived from the service name. That convention holds for every service but one, and the exception answers 404 — which reads as "no such endpoint" rather than "right address, wrong face".
func (Endpoints) ServiceURL ¶
ServiceURL is where a service answers.
Resolution order: an explicit override, then the contract's own address with Domain applied if one is set, then the contract's address as written. A contract that states no address produces an error naming the contract, because that is where the fix belongs.