Skip to content

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 ​

TermDefinitionCode anchor
Cloud ObjectAn 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
SourceThe 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
ListingThe 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
ListerThe 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:

  1. Read the credential's metadata (its home Cloud and the KV version of its material) through the CredentialReader port. A revoked or expired credential is refused here.
  2. Read the home Cloud through the CloudReader port.
  3. Refuse a source whose provider is not the Cloud's provider (ErrProviderMismatch).
  4. Refuse a provider that has no lister (ErrNoLister).
  5. Answer from the cache when the same credential, KV version and source were listed in the last 60 seconds.
  6. Reveal the credential's plaintext through the CredentialReader port. The composition root adapts this onto Custodian.Reveal, which reads the KV version the credential's row records and audits the read with the relation revealed and the lookup's purpose.
  7. 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.
  8. Drop objects without an id, logging one WARN that counts them.
  9. Sort the rest by lower-cased name, then id.
  10. 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:

SourceCallDetail
openstack.flavorNova flavors.ListDetail<vCPUs> vCPU, <RAM> GiB RAM, <disk> GB disk
openstack.imageGlance images.List with status=active and a page limit of one past the listing's limit, following the next linkthe visibility, and min disk <n> GB when the image sets one
openstack.networkNeutron networks.List with a page limit of one past the listing's limit, following the next linkexternal and shared when set
openstack.external-networkNeutron networks.List with router:external=true and the same page limitas for openstack.network
openstack.availability-zoneNova availabilityzones.Listavailable 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 ​

ErrorMeaningHTTP answer
ErrUnknownSourceThe source is outside the closed set.400 invalid_parameter_source
ErrCredentialNotFoundNo credential row has the id.404 cloud_credential_not_found
ErrCredentialUnusableThe credential is revoked or expired, its material is gone, or its payload is not the provider's credentials document.409 cloud_credential_unusable
ErrProviderMismatchThe source belongs to another provider than the credential's home Cloud.422 parameter_source_provider_mismatch
ErrCredentialStoreUnavailableThe material could not be read: OpenBao is down, or the API's policy lacks read on the credential paths.503 cloud_credential_store_unavailable
*UpstreamErrorThe Cloud did not answer the listing; its kind says why.502 cloud_objects_upstream_error
ErrCloudNotFound, ErrNoListerThe 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:

KindWhenDetail
auth_refusedKeystone answered 401 or 403.the cloud refused the credential
list_refusedA list call answered 401 or 403.the cloud refused to list these objects
no_endpointThe 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
unreachableThe deadline passed, the connection failed, or the egress guard refused the address.the cloud could not be reached
malformedA body did not decode, exceeded 8 MiB, or came with another status.the cloud's answer could not be read

Metrics ​

MetricLabelsMeaning
plexsphere_cloud_objects_lookups_totalsource, outcomeOne per lookup. outcome is listed, cached, provider_mismatch, credential_unusable, credential_store_unavailable, upstream_error or error.
plexsphere_cloud_objects_upstream_duration_secondssourceHistogram 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.