Documentation
¶
Overview ¶
Package cloudcontrol is a client for AWS's Cloud Control API: the transport the registry-backed discovery plane (#40) reads live resources through. It mirrors the semantics of chant's proven implementation (chant/lexicons/aws/src/api/read-client.ts's Cloud Control half), ported to Go rather than translated line for line.
Cloud Control speaks AWS JSON 1.0: a POST with an X-Amz-Target header naming the operation (CloudApiService.ListResources, CloudApiService.GetResource) and a JSON body, against https://cloudcontrolapi.<region>.amazonaws.com or an endpoint override (floci: http://localhost:4566). Client wraps both operations, paginating ListResources to exhaustion, and reports failures as *APIError so a caller classifies them by the API's own error code rather than by parsing prose.
Signing ¶
Requests are signed with SigV4 when credentials resolve, using aws-sdk-go-v2's default credential chain unless a Client is given its own aws.CredentialsProvider. Two cases stay unsigned, deliberately:
- No credentials resolve. The request goes out carrying only the region-scope placeholder described below.
- An endpoint override is set and Config.SignEndpointOverride is false. Floci does not verify signatures, and signing against it would mean every local run suddenly needs credentials to read what it just deployed. SignEndpointOverride opts back in for an override that is itself real AWS — a VPC endpoint, a signing proxy.
Every unsigned request still carries a region-scope placeholder in its Authorization header: an emulator has one host for every region, so without it a multi-region estate reads as one region silently, the same failure mode chant's regionScope comment documents. The placeholder is not a signature — real AWS rejects it — it is only what an emulator reads the region out of the way a real SDK would carry it.
Retries ¶
Every call (Client.ListResources's per-page calls, Client.GetResource, Client.GetResources) retries under one policy, stated here because issue #64 asked for a stated policy rather than an implicit one:
- Only a ThrottlingException response is retried. Every other failure - a different *APIError code (ResourceNotFoundException, ValidationException, UnsupportedOperation, ...), a transport error, a response this client could not parse - returns to the caller straight from the first attempt. Retrying a validation error or a resource-not-found result would not change the outcome; it would only delay reporting it.
- Exponential backoff with full jitter: attempt N's delay (before attempt N+1) is drawn uniformly from [0, min(RetryMaxDelay, RetryBaseDelay*2^(N-1))). Full jitter - not a fixed curve - is AWS's own documented recommendation for avoiding synchronized retry storms, which matters here because discovery issues its GetResource calls concurrently: a fixed curve would have every one of them retry in lockstep against the same throttled API.
- Bounded attempts: Config.MaxAttempts (default 5, counting the first try) caps the total, so a call that keeps getting throttled fails rather than retrying forever.
- Context-respecting: a retry's sleep ([retrySleep]) selects on ctx.Done() alongside its timer, so a canceled or deadline-exceeded context stops the retry loop immediately - mid-sleep, if that is where it is - rather than finishing out the backoff curve first.
Config.RetryBaseDelay (default 200ms) and Config.RetryMaxDelay (default 5s) tune the curve; Config.RetrySleep overrides the sleep function itself, which is how a unit test gets a deterministic, instant backoff instead of a real one (internal/live/cloudcontrol/retry_test.go).
Resource Groups Tagging API ¶
NewTagging builds a Client for a second service that speaks the same AWS JSON RPC shape against a different host, target namespace and protocol version: tagging.<region>.amazonaws.com, ResourceGroupsTaggingAPI_20170126, Content-Type application/x-amz-json-1.1 rather than Cloud Control's 1.0 (see tagging.go's taggingContentType - floci enforces the distinction even though X-Amz-Target alone already names the operation). GetResources (Client.GetResources) is the estate-wide sweep primitive issue #47 evaluated: one paginated call returns every ARN carrying an estate's tofu-estate tag, in place of a ListResources call per admitted type. Issue #51 wires it into internal/live/discovery's sweep behind Request.TaggingSweep, joining each returned ARN back to a (TF type, identifier) pair (internal/live/discovery/tagging.go).
Index ¶
- Constants
- func HasCode(err error, code string) bool
- func JoinIdentifier(parts ...string) string
- type APIError
- type ARN
- type Client
- func (c *Client) GetResource(ctx context.Context, typeName, identifier string) (*ResourceDescription, error)
- func (c *Client) GetResources(ctx context.Context, resourceTypeFilters []string, tagFilters []TagFilter) ([]TaggedResource, error)
- func (c *Client) ListResources(ctx context.Context, typeName string) ([]ResourceDescription, error)
- func (c *Client) ListResourcesScoped(ctx context.Context, typeName string, resourceModel map[string]string) ([]ResourceDescription, error)
- type Config
- type ResourceDescription
- type TagFilter
- type TaggedResource
Constants ¶
const ( CodeUnsupportedOperation = "UnsupportedOperation" CodeResourceNotFoundError = "ResourceNotFoundException" CodeValidationError = "ValidationException" CodeThrottlingError = "ThrottlingException" )
The error codes Cloud Control sends that a caller of this package needs to tell apart. There are more codes than these in the API, but these four are the ones the discovery layer branches on: UnsupportedOperation is floci's answer for GetResource on some types while ListResources works for the same type, and the other three separate "this type does not exist here" from "the request was malformed" from "slow down".
Variables ¶
This section is empty.
Functions ¶
func HasCode ¶
HasCode reports whether err is (or wraps) an *APIError whose Code is code, so a caller can write
if cloudcontrol.HasCode(err, cloudcontrol.CodeUnsupportedOperation) { ... }
instead of its own errors.As boilerplate for the common case of checking one specific code.
func JoinIdentifier ¶
JoinIdentifier joins a multi-part resource identifier with Cloud Control's own separator, "|" — for example the two-part key an aws_route_table_association needs: JoinIdentifier(routeTableID, subnetID).
Types ¶
type APIError ¶
type APIError struct {
// Op is the operation that failed: "ListResources" or "GetResource".
Op string
// StatusCode is the response's HTTP status.
StatusCode int
// Code is the API's error code, e.g. CodeResourceNotFoundError.
Code string
// Message is the API's own explanation, when it sent one.
Message string
}
APIError is a failed Cloud Control call, carrying the HTTP status and the API's own error code (its "__type", stripped of the shape-ID prefix Cloud Control puts in front of it) so a caller classifies the failure without parsing Message.
Code is empty when the response carried no __type at all — an HTTP failure Cloud Control did not explain, or a response this client could not parse as JSON.
type ARN ¶
type ARN struct {
Partition string
Service string
Region string
Account string
// ResourceType is the segment naming the resource's kind within its
// service, when the ARN's resource field carries one: the "role" of
// arn:aws:iam::111111111111:role/deploy, the "log-group" of
// arn:aws:logs:us-east-1:111111111111:log-group:my-group. Empty when the
// resource field is a bare identifier with no type segment at all - an
// S3 bucket ARN (arn:aws:s3:::NAME) or an SNS topic ARN
// (arn:aws:sns:REGION:ACCOUNT:NAME) carry only the name.
ResourceType string
// ResourceID is the resource's own identifier: the part of the resource
// field after ResourceType's separator, or the whole resource field when
// it carried no type segment. It may itself contain further "/" or ":"
// characters - an ELBv2 load balancer's id is "app/NAME/HASH", an IAM
// role with a path is "PATH/NAME" - because only the first separator in
// the resource field divides type from id; nothing past it is split
// again.
ResourceID string
}
ARN is one AWS ARN, split into the fields https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html documents: arn:partition:service:region:account-id:resource.
func ParseARN ¶
ParseARN splits an AWS ARN into ARN's fields, and its resource field further into a type and an id wherever the resource shows one.
The Tagging API's GetResources (tagging.go's Client.GetResources) hands back real ARNs, and the AWS ARN reference is explicit that the resource field's own shape varies by service. Three shapes cover every service this fork's identity table admits:
- "type/id" - an IAM role (role/NAME), an EC2 VPC (vpc/vpc-ID), an ELBv2 target group (targetgroup/NAME/HASH, whose id itself carries a "/").
- "type:id" - a Lambda function (function:NAME), a CloudWatch Logs log group (log-group:NAME), a Step Functions state machine (stateMachine:NAME).
- a bare id, no type segment at all - an S3 bucket (arn:aws:s3:::NAME) or an SNS topic (arn:aws:sns:REGION:ACCOUNT:NAME).
Only the first "/" or ":" in the resource field - whichever comes first - divides type from id, and every admitted service uses at most one of the two separators to mean type/id at all, so a multi-segment id (the ELBv2 case above) and a log group name that itself starts with "/" both stay inside ResourceID rather than being cut again.
ok is false for anything that does not even have the six-colon-field arn:partition:service:region:account:resource shape with a literal "arn" first, or whose resource field is empty. Parsing never guesses at a shape a malformed string does not have.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a Cloud Control API client: two operations (ListResources, GetResource), each a POST of AWS JSON 1.0 against Cloud Control's endpoint, signed with SigV4 when credentials resolve. See the package doc for the signing and endpoint-override rules.
func New ¶
New builds a Client from cfg, configured for Cloud Control's own two operations (ListResources, GetResource).
func NewTagging ¶
NewTagging builds a Client for the Resource Groups Tagging API's GetResources operation (issue #47's estate-wide sweep primitive) instead of Cloud Control's ListResources/GetResource, reusing the same endpoint-override and signing rules New and doc.go document. Only Client.GetResources is meaningful on the result; ListResources and GetResource would send Cloud Control's operations against the tagging host and get nowhere.
func (*Client) GetResource ¶
func (c *Client) GetResource(ctx context.Context, typeName, identifier string) (*ResourceDescription, error)
GetResource fetches the full live model for one identifier of typeName.
func (*Client) GetResources ¶
func (c *Client) GetResources(ctx context.Context, resourceTypeFilters []string, tagFilters []TagFilter) ([]TaggedResource, error)
GetResources enumerates every resource carrying tagFilters, across every resource type unless resourceTypeFilters narrows it, paginating PaginationToken to exhaustion - the Resource Groups Tagging API's estate-wide sweep: one paginated call returns every ARN carrying an estate's tofu-estate marker, rather than a Cloud Control ListResources call per admitted type (internal/live/discovery's sweepTypes).
TODO(#47): nothing in this package or in internal/live/discovery calls this yet. Turning a TaggedResource's ARN into the (resource type, identifier) pair the marker-discovery bind step needs - parsing the ARN's service and resource segments and joining them against live/mapping.json/live/registry.json - is the piece #47 scoped out as a stretch goal; see the issue comment recording that decision. The client call itself (this function, tested against a fake server in tagging_test.go) is the part #47 commits to.
func (*Client) ListResources ¶
ListResources enumerates every live resource of typeName, paginating NextToken to exhaustion. It sends no ResourceModel at all, which Cloud Control accepts only for a type whose list handler needs no scoping input; a type whose handler requires one (live/registry.json's handlers.list_required_input) answers with a validation error rather than an unscoped enumeration - see Client.ListResourcesScoped for that case.
func (*Client) ListResourcesScoped ¶
func (c *Client) ListResourcesScoped(ctx context.Context, typeName string, resourceModel map[string]string) ([]ResourceDescription, error)
ListResourcesScoped is Client.ListResources for a type whose list handler requires scoping input - the composite identities under a config-known parent that CloudFormation's own ListResources documents list_required_input for (e.g. AWS::ApiGateway::Resource needs RestApiId): ordinary ListResources would either enumerate every parent's children in one unfiltered call (wrong: no way to tell which child belongs to which parent without a second read per result) or, for a handler that mandates the input, fail outright.
resourceModel is the partial resource model Cloud Control scopes the listing to, keyed by the CFN property name(s) the type's own list_required_input names - e.g. {"RestApiId": "abc123"} - marshaled to the JSON string the wire's ResourceModel field carries (verified against the service's own API model: ListResourcesInput.ResourceModel is shape Properties, a JSON string, the same encoding ResourceDescription.Properties already round-trips on the read side). An empty map is rejected rather than silently sent as "{}": a caller asking to scope with nothing to scope by is a bug in the caller, not a request Cloud Control should ever see, and a type that genuinely needs no scoping belongs on Client.ListResources instead.
Cloud Control is documented to filter server-side on the fields resourceModel supplies, but this client does not assume every backend honors that - floci's own ListResources implementation ignores ResourceModel entirely and returns every live resource of typeName unfiltered, verified by reading its handler (CloudControlJsonHandler.listResources delegates straight to CloudControlService.listResources(region, typeName), no ResourceModel parameter in the call at all). A caller that trusts an unscoped result list is scoped just because it sent scoping risks attributing one parent's children to a different parent - exactly the removal-detection hazard this method exists to avoid, not commit by omission. Every result this call returns must still be verified against the same scoping value before being attributed to a parent; this client sends the scope but makes no promise about what came back.
type Config ¶
type Config struct {
// Endpoint overrides the real AWS host, e.g. floci's
// "http://localhost:4566". Empty means real AWS.
Endpoint string
// Region selects the real-AWS host and the SigV4 credential scope.
// Empty means defaultRegion.
Region string
// Credentials is what to sign with. Nil defers to aws-sdk-go-v2's
// default credential chain (environment, shared config, IMDS, ...),
// resolved lazily on first use. When nothing resolves, requests go out
// unsigned.
Credentials aws.CredentialsProvider
// SignEndpointOverride signs even when Endpoint is set, for an override
// that is itself real AWS — a VPC endpoint, a signing proxy. Floci does
// not verify signatures, so the default (false) is what every local run
// wants: unsigned requests against the emulator, no credentials
// required to read back what was just written.
SignEndpointOverride bool
// RoundTripper is what requests are sent over. Defaults to
// http.DefaultTransport.
RoundTripper http.RoundTripper
// HTTPTimeout bounds one HTTP attempt end to end - dial, send, response
// body. Zero means defaultHTTPTimeout (30s). Without a bound, one
// unresponsive host parks a discovery scan indefinitely: the ctx most
// callers pass has no deadline of its own.
HTTPTimeout time.Duration
// Now is the signing clock. Defaults to time.Now; tests inject it for a
// reproducible signature and a reproducible region-scope placeholder.
Now func() time.Time
// MaxAttempts bounds how many times one call may attempt the request
// before giving up, counting the first try. Only a ThrottlingException
// response triggers a retry at all (see doc.go's "Retries" section);
// every other failure - including a different *APIError code, a
// transport error, a malformed response - returns to the caller after
// the first attempt. Zero means defaultMaxAttempts (5).
MaxAttempts int
// RetryBaseDelay is the backoff curve's starting point: the first
// retry's delay is uniformly random between 0 and this value, doubling
// (still full-jitter) on each attempt after that, up to RetryMaxDelay.
// Zero means defaultRetryBaseDelay (200ms).
RetryBaseDelay time.Duration
// RetryMaxDelay caps any single retry's sleep. Zero means
// defaultRetryMaxDelay (5s).
RetryMaxDelay time.Duration
// RetrySleep overrides the function a retry waits with between
// attempts - a test's hook for a deterministic, instant backoff curve
// instead of a real sleep. Nil means retrySleep, which respects ctx
// cancellation.
RetrySleep func(ctx context.Context, d time.Duration) error
}
Config configures a Client. The zero value is a client for real AWS in defaultRegion, unsigned unless the environment's default credential chain resolves something.
type ResourceDescription ¶
type ResourceDescription struct {
// Identifier is the resource's Cloud Control identifier. A multi-part
// identifier arrives already joined with "|"; see [JoinIdentifier] for
// building one to send.
Identifier string
// Properties is the resource's model, decoded from the JSON string
// Cloud Control wraps it in. Nil when the response carried no
// Properties at all, which is not an error — see
// wireResourceDescription.decode for the cases this client does treat
// as one.
Properties map[string]any
}
ResourceDescription is one live resource as Cloud Control describes it.
func GetResourceByIdentity ¶
func GetResourceByIdentity(ctx context.Context, c *Client, typeName, identifier string) (*ResourceDescription, error)
GetResourceByIdentity fetches one resource by identity, falling back to a full ListResources plus an identifier match when GetResource itself refuses with CodeUnsupportedOperation - floci's exact answer for some types while ListResources on the same type works fine (see errors.go).
This is chant's proven readByIdentity (identity-observe.ts) ported to Go: every other refusal from GetResource propagates unchanged, because the caller - not this function - decides what a failed refinement means; only UnsupportedOperation specifically degrades to the list-and-match path rather than surfacing as an error. A miss in the fallback list (the identifier is genuinely not present) is reported as (nil, nil): a confirmed absence, not a failure.
type TagFilter ¶
TagFilter is one GetResources tag-filter term: match anything carrying Key, optionally narrowed to one of Values (an empty Values matches the key with any value, or no value at all).
type TaggedResource ¶
TaggedResource is one GetResources hit: a resource ARN and the tags it carries.
What this type deliberately does not do is turn ResourceARN into a (resource type, identifier) pair discovery could bind against. An ARN's service and resource segments would need to be joined against live/mapping.json and live/registry.json the same way internal/live/discovery's per-type Cloud Control path already does with a CFN type name in hand, and that join is scoped out of the #47 batch - see the TODO on Client.GetResources. This type carries the raw material (the ARN, the tags) for whichever caller takes that on.