Appearance
Cloud Objects
The Cloud Objects sub-context lists, live, the objects a Cloud Credential sees in its home Cloud. A blueprint parameter can declare a parameter source, such as openstack.image; when a Project member creates a provisioned Resource in the console and has picked the Blueprint version and the Cloud Credential, every sourced parameter becomes a searchable list of those objects. The code lives in internal/provisioning/cloudobjects/ and its OpenStack lister in internal/provisioning/cloudobjects/openstack/. The HTTP operation is ListCloudObjects, described in the Cloud Credentials API reference.
The list is advisory. Admission checks a parameter value against the version's parameter schema alone, so a Resource can be created while a Cloud cannot be listed, and the console falls back to free text.
Ubiquitous language
| Term | Definition | Code anchor |
|---|---|---|
| Cloud Object | An object that exists in a Cloud and that a parameter can name: a flavor, an image, a network, an availability zone. It has an id, the value a client submits; a name, which may be empty and need not be unique; and an optional short detail such as a flavor's size. | object.go |
| Source | The kind of Cloud Object a parameter takes the id of, such as openstack.flavor. The text before the first . is the provider the source belongs to. A closed set, kept equal to the Blueprint Catalog's parameter sources. | source.go |
| Listing | The Cloud Objects of one source, sorted by lower-cased name and then id, with a Truncated flag set when the Cloud holds more than the list carries. | object.go |
| Lister | The port one provider implements to list its sources. | ports.go |
The sub-context keeps its own Source enum rather than importing the Blueprint Catalog's ParameterSource: the internal/provisioning module does not require the blueprints module, and each provisioning sub-context keeps its model behind its own seam. A test in cmd/plexsphere, the one place that imports both, pins that the two sets are equal and that every source's provider has a lister.
The flow
Service.List(credential, source) runs these steps in order:
- Read the credential's metadata (its home Cloud and the KV version of its material) through the
CredentialReaderport. A revoked or expired credential is refused here. - Read the home Cloud through the
CloudReaderport. - Refuse a source whose provider is not the Cloud's provider (
ErrProviderMismatch). - Refuse a provider that has no lister (
ErrNoLister). - Answer from the cache when the same credential, KV version and source were listed in the last 60 seconds.
- Reveal the credential's plaintext through the
CredentialReaderport. The composition root adapts this ontoCustodian.Reveal, which reads the KV version the credential's row records and audits the read with the relationrevealedand the lookup's purpose. - Call the provider's lister with a limit of 1,000 objects under a 20-second deadline, inside the API router's 30-second request budget.
- Drop objects without an id, logging one WARN that counts them.
- Sort the rest by lower-cased name, then id.
- Store the listing in the cache and return it.
Steps 3 and 4 run before any plaintext is read. Every port error comes back wrapped, and a failed listing is never cached, so the next request tries again.
The cache
The cache holds successful listings for 60 seconds, at most 512 entries, in memory on each API replica. Its key is the credential id, the KV version of the credential's material and the source, so a rotation of the credential misses it. On insert it drops every expired entry and then, when still full, the oldest one. A cache shared across replicas was rejected: a listing is small and advisory, and a cold replica costs one upstream call per credential and source per minute.
The OpenStack sources
The OpenStack lister decodes the credentials document the Issue a Cloud Credential guide describes (auth_url, region, application_credential_id, application_credential_secret). An empty region falls back to the Cloud's default_region. It authenticates against Keystone with the application credential, picks each service's public endpoint in that region from the token's catalog, and calls gophercloud:
| Source | Call | Detail |
|---|---|---|
openstack.flavor | Nova flavors.ListDetail | <vCPUs> vCPU, <RAM> GiB RAM, <disk> GB disk |
openstack.image | Glance images.List with status=active and a page limit of one past the listing's limit, following the next link | the visibility, and min disk <n> GB when the image sets one |
openstack.network | Neutron networks.List with a page limit of one past the listing's limit, following the next link | external and shared when set |
openstack.external-network | Neutron networks.List with router:external=true and the same page limit | as for openstack.network |
openstack.availability-zone | Nova availabilityzones.List | available zones only; a zone's id is its name |
Paging stops once the lister holds one object more than the limit; that object marks the listing truncated and the first 1,000 are returned. Each object's id, name and detail is bounded at 1,020 bytes (255 characters of UTF-8): a longer name or detail is cut, and an object with a longer id is dropped with the WARN line for objects without an id. A payload that is not this document (invalid JSON, no auth_url, no application credential, no region after the fallback) is ErrCredentialUnusable, and no error message quotes a payload byte.
Errors
| Error | Meaning | HTTP answer |
|---|---|---|
ErrUnknownSource | The source is outside the closed set. | 400 invalid_parameter_source |
ErrCredentialNotFound | No credential row has the id. | 404 cloud_credential_not_found |
ErrCredentialUnusable | The credential is revoked or expired, its material is gone, or its payload is not the provider's credentials document. | 409 cloud_credential_unusable |
ErrProviderMismatch | The source belongs to another provider than the credential's home Cloud. | 422 parameter_source_provider_mismatch |
ErrCredentialStoreUnavailable | The material could not be read: OpenBao is down, or the API's policy lacks read on the credential paths. | 503 cloud_credential_store_unavailable |
*UpstreamError | The Cloud did not answer the listing; its kind says why. | 502 cloud_objects_upstream_error |
ErrCloudNotFound, ErrNoLister | The home Cloud has no row, or no lister lists the source. | 500 |
An *UpstreamError unwraps to ErrUpstream and to its cause, and its text names the kind only. The service logs the cause at WARN. The kinds and the fixed sentence the HTTP detail carries for each:
| Kind | When | Detail |
|---|---|---|
auth_refused | Keystone answered 401 or 403. | the cloud refused the credential |
list_refused | A list call answered 401 or 403. | the cloud refused to list these objects |
no_endpoint | The catalog has no public endpoint of the service in the region. | the cloud's catalog lists no endpoint for this source in the credential's region |
unreachable | The deadline passed, the connection failed, or the egress guard refused the address. | the cloud could not be reached |
malformed | A body did not decode, exceeded 8 MiB, or came with another status. | the cloud's answer could not be read |
Metrics
| Metric | Labels | Meaning |
|---|---|---|
plexsphere_cloud_objects_lookups_total | source, outcome | One per lookup. outcome is listed, cached, provider_mismatch, credential_unusable, credential_store_unavailable, upstream_error or error. |
plexsphere_cloud_objects_upstream_duration_seconds | source | Histogram of the lister call, observed only when the lister runs. |
The egress guard
The auth URL and the catalog endpoints come from an operator-supplied credential, so the lister's HTTP client is built over GuardedTransport. Its dialer refuses loopback, link-local (unicast and multicast), unspecified and multicast addresses, and the cloud metadata addresses outside link-local (fd00:ec2::254, fd20:ce::254, 100.100.100.200, and Azure's WireServer at 168.63.129.16), after name resolution, so a name that resolves to a refused address is refused too. Every response body is cut at 8 MiB, the client follows no redirect, and a request times out after 20 seconds. Private ranges (RFC 1918, IPv6 unique-local, the shared 100.64.0.0/10 space) stay reachable apart from those metadata addresses, because on-premises OpenStack commonly runs on them. With a proxy configured through the environment, the guard sees the proxy's address and the proxy decides where the request goes.
Why the broker still never reads plaintext
The Provisioning Broker hands a credential to Crossplane through an ExternalSecret and never reads its material. The lookup does not change that: it is the only consumer of Custodian.Reveal, it runs in the API process, and its response carries ids, names and details only. The read needs read on the credential paths in the API's OpenBao policy (see Install with Helm). A deployment without it keeps creating Resources, and every lookup answers 503 cloud_credential_store_unavailable with a WARN naming the missing capability.