Glance Operator
The Glance operator deploys and manages the OpenStack Image service as a Kubernetes-native workload. It is the third service operator built on the shared scaffolding the Keystone operator established (internal/common, the operator-library Helm chart, the parameterized operator image), after Keystone and Horizon.
Glance is the first service operator that is a Keystone API consumer: the API server runs with a [keystone_authtoken] service user and validates a Keystone token on every request, so a plain spec.keystoneEndpoint URL points the pods at the auth endpoint they reach server-side. Its other distinguishing trait is state: images live in a shared object store, and each store is modeled out-of-band as a separate GlanceBackend CR rather than inline in the Glance spec, so stores can be attached and detached without editing the Glance CR.
Design decisions
The v1 operator resolves the onboarding decisions as follows:
- Release-switched launch mode.
spec.openStackReleasegoverns how the API server launches: the eventletglance-apiserver below2026.1, and uWSGI from2026.1onward. The uWSGI mode loads the stock WSGI app through the image-shippedglance-wsgi-apishim, because glance's own WSGI module ignoressys.argvand cannot be pointed at the operator's config directories directly. The launch mode is deliberately decoupled from the image tag so a digest-pinned image still resolves a schema and launch mode. See Container Images. - Always-rendered reserved stores. Glance registers the
os_glance_staging_storeandos_glance_tasks_storefilesystem stores at/var/lib/glance/stagingand/var/lib/glance/tasks-workon every deployment, even an all-object-store one: import staging and async task work land on local disk regardless of the image store. Both areemptyDirs bounded by a defaultsizeLimitof 10Gi (spec.staging.sizeLimit), so an oversized import gets the glance-api pod evicted rather than running until the node's disk is full; see StagingSpec for what that bound does and does not guarantee.enabled_import_methodsis rendered as[web-download,copy-image]—glance-directis deliberately excluded because it stages the uploaded image on the API pod's local disk, and there is no staging volume shared across replicas, so an import begun on one pod could not be finished by another. Every deployment also renders an[import_filtering_opts]group, so aweb-downloadURI is filtered before glance fetches it: HTTPS on port 443, plus a literal host denylist covering loopback, the link-local metadata address, and the in-cluster API server. See ImportFilteringSpec. - Image cache: sqlite driver, bounded
emptyDir, sidecar pruner.spec.imageCacheturns on a per-replica cache of the image data the API has served. The driver is pinned tosqliteeven though upstream deprecates it in favour ofcentralized_db: that default keys cache state in the Glance database perworker_self_reference_url, so with Deployment pods whose names change on every replacement it strandsnode_referenceandcached_imagesrows there and turns every cache hit into a database write.sqlitekeeps the metadata inside the cache directory, where it shares the volume's lifecycle. The directory is a boundedemptyDirand not a PVC, so the operator assumes no StorageClass, reuses the bounded-scratch pattern the staging volumes already establish, and avoids a bound that would be fiction on kind, whose local-path provisioner enforces no capacity. Pruning runs in acache-maintenancesidecar because a CronJob cannot reach a pod-local volume. See ImageCacheSpec. - Import plugins: an opt-in block per plugin, rendered in a fixed order.
spec.importPluginsselects the image-import plugins, and the presence of a sub-block is the switch for its plugin. The rendered order (image_decompression,image_conversion,inject_image_metadata) is the operator's and not an input, which is what satisfies upstream's one ordering requirement: decompression has to precede conversion, or conversion would rewrite the archive instead of the disk image inside it.image_import_pluginsis rendered on every deployment,[]while the block is unset, so a missing key never leaves Glance on its own default. The plugins are stages of the interoperable import flow, so aPUT /v2/images/{id}/fileupload bypasses them andcopy-imageskips them, which leavesweb-downloadimports. The Glance image shipsqemu-imgandlhafile, the two the conversion and decompression plugins shell out to and import at run time. See ImportPluginsSpec. isDefaultlives on the backend CR. Exactly one attached, credential-readyGlanceBackendmust be markedisDefault; that backend becomes the[glance_store] default_backend. The glance-side sub-reconciler and a sibling-uniqueness webhook both enforce the single-default invariant.- Keystone endpoints: two plain URL fields.
spec.keystoneEndpointrenders as[keystone_authtoken] auth_url(the pod-reachable, server-side URL) and the optionalspec.keystonePublicEndpointaswww_authenticate_uri(the browser/client-facing address a 401 points at). The service-user password is never rendered into config: it is delivered as theOS_KEYSTONE_AUTHTOKEN__PASSWORDenvironment variable, digested into a pod-template annotation so a rotation rolls the pods. /healthcheckprobes. Readiness and liveness both GET/healthcheck, served by the oslo healthcheck middleware without touching the database or Keystone, identical in both launch modes.- Expand-migrate-contract upgrades. When
spec.openStackReleaseadvances to a new OpenStack release (with the image in lockstep), the operator drives phased database migrations while the API keeps serving. Sequential-only upgrade paths; a fresh install or a same-release image bump stays on the single-passglance-manage db sync. See Upgrade Flow. - No live S3 probing by the operator. The operator never connects to an S3 endpoint to validate a backend; it only resolves the credentials Secret and renders the store section. Bucket reachability is a runtime concern of the Glance pods.
Owned resources
For a Glance CR named {name} the operator manages:
| Resource | Name | Purpose |
|---|---|---|
| Deployment | {name} | The Glance API pods (port 9292) |
| Service | {name} | ClusterIP in front of the API pods on port 9292 |
| PodDisruptionBudget | {name} | minAvailable: 1 (or maxUnavailable: 1 at a single replica) |
| HorizontalPodAutoscaler | {name} | Only when spec.autoscaling is set |
| NetworkPolicy | {name} | Only when spec.networkPolicy is set |
| HTTPRoute | {name} | Only when spec.gateway is set |
| ConfigMap | {name}-config-<hash> | Immutable, content-addressed glance-api.conf / glance-api-paste.ini (3 historical retained) |
| Secret | {name}-backends-<hash> | Immutable, content-addressed backends.conf (the aggregated store sections; 3 historical retained) |
| Secret | {name}-db-connection | Derived pymysql DSN, consumed via OS_DATABASE__CONNECTION |
| Job | {name}-db-sync | glance-manage db sync schema migration |
| CronJob | {name}-db-purge | Scheduled purge of soft-deleted task rows (image rows opt-in) |
Reference pages
- Glance CRD — the full
spec/statuscontract - GlanceBackend CRD — the per-store attachment CRD
- Controller Events — the Kubernetes events the controller emits
- Reconciler Architecture — the sub-reconciler pipeline, conditions, and requeue semantics
- Upgrade Flow — the expand-migrate-contract release-upgrade machine