Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/build-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ name: Build Images
- patches/**
- scripts/**
- hack/ci-*
- hack/gen-option-catalog.sh
- operators/keystone/api/v1alpha1/catalogs/**
- operators/glance/api/v1alpha1/catalogs/**
- .github/actions/**
- tests/container-images/**
- .github/workflows/build-images.yaml
Expand All @@ -28,6 +31,9 @@ name: Build Images
- patches/**
- scripts/**
- hack/ci-*
- hack/gen-option-catalog.sh
- operators/keystone/api/v1alpha1/catalogs/**
- operators/glance/api/v1alpha1/catalogs/**
- .github/actions/**
- tests/container-images/**
- .github/workflows/build-images.yaml
Expand Down Expand Up @@ -566,6 +572,15 @@ jobs:
verify-script: ${{ github.event_name == 'pull_request' && format('tests/container-images/verify_{0}.sh', matrix.service) || '' }}
verify-image-ref: ${{ steps.tags.outputs.composite }}

- name: Verify option catalog
if: github.event_name == 'pull_request' && (matrix.service == 'keystone' || matrix.service == 'glance')
env:
MATRIX_SERVICE: ${{ matrix.service }}
MATRIX_RELEASE: ${{ matrix.release }}
IMAGE_REF: ${{ steps.tags.outputs.composite }}
SOURCE_DIR: src/${{ matrix.service }}
run: hack/gen-option-catalog.sh --check "$MATRIX_SERVICE" "$MATRIX_RELEASE" "$IMAGE_REF"

# Assembles per-platform service digests into multi-arch manifests with attestation.
merge-service-images:
if: github.event_name != 'pull_request'
Expand Down Expand Up @@ -719,3 +734,11 @@ jobs:
run: |
docker pull "$IMAGE_REF"
bash "tests/container-images/verify_${SERVICE_NAME}.sh" "$IMAGE_REF"

- name: Verify option catalog
if: matrix.service == 'keystone' || matrix.service == 'glance'
env:
MATRIX_SERVICE: ${{ matrix.service }}
MATRIX_RELEASE: ${{ matrix.release }}
IMAGE_REF: ${{ steps.tags.outputs.composite }}
run: hack/gen-option-catalog.sh --check "$MATRIX_SERVICE" "$MATRIX_RELEASE" "$IMAGE_REF"
3 changes: 3 additions & 0 deletions .github/workflows/verify-container-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ name: Verify Container Images
- patches/**
- scripts/**
- hack/tempest/**
- hack/gen-option-catalog.sh
- operators/keystone/api/v1alpha1/catalogs/**
- operators/glance/api/v1alpha1/catalogs/**
- tests/container-images/**
- tests/scripts/**
- tests/tempest/test_retry_helpers.py
Expand Down
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,36 @@ verify-invalid-cr-fixtures:
@python3 tests/e2e/glance/invalid-cr/test_generate.py
@python3 tests/e2e/glance/invalid-glancebackend-cr/test_generate.py

.PHONY: gen-option-catalogs
# gen-option-catalogs regenerates the per-release option catalogs the
# extraConfig validation checks against. For every releases/<release>/ directory
# it runs hack/gen-option-catalog.sh for keystone and glance, extracting each
# catalog from the matching shipped service image via that image's own
# oslo-config-generator. Requires docker and network access: it pulls the
# service image when absent and fetches the upstream generator config. It is
# intentionally wired into no aggregate target and no CI job.
gen-option-catalogs:
@for release in $(notdir $(patsubst %/,%,$(wildcard releases/*/))); do \
for service in keystone glance; do \
echo "Generating $$service $$release option catalog..."; \
hack/gen-option-catalog.sh $$service $$release; \
done; \
done

.PHONY: verify-option-catalogs
# verify-option-catalogs asserts the committed option catalogs stay byte-stable
# against a fresh extraction. It runs the same loop as gen-option-catalogs with
# --check, so a drifted or missing catalog fails the target with a diff. Like
# gen-option-catalogs it requires docker and network access and is wired into no
# aggregate target and no CI job.
verify-option-catalogs:
@for release in $(notdir $(patsubst %/,%,$(wildcard releases/*/))); do \
for service in keystone glance; do \
echo "Checking $$service $$release option catalog..."; \
hack/gen-option-catalog.sh --check $$service $$release; \
done; \
done

.PHONY: check-feature-ids
# check-feature-ids fails if any internal feature / requirement ID (CC-NNNN or
# REQ-NNN) appears anywhere in the tracked source tree — code, tests, CI,
Expand Down
41 changes: 38 additions & 3 deletions docs/guides/advanced-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,44 @@ it from the typed `spec.logging.debug` field — so this example now draws an
`ExtraConfigOwnedKeyOverride` Warning event and flips `ExtraConfigHealthy` to
`False` while still taking effect.

The operator does not validate the content of these sections. A typo becomes a silent
no-op at best and a crash loop at worst — test changes in a lab before rolling out.
A change to `extraConfig` triggers a ConfigMap rehash and a rolling Deployment update.
Beyond the ownership registry, the validating webhook checks every option name in
`spec.extraConfig` against a catalog of the options the service actually accepts.
Each operator embeds one catalog per OpenStack release, generated from the
`oslo-config-generator` run of the exact service image forge ships for that
release. The catalog answers a single question: does this option name exist?
Values are never inspected. Keystone derives the release from `spec.image.tag`;
Glance derives it from `spec.openStackRelease`. An option that sits in a known
section but is absent from the catalog is rejected at apply time with `no such
option in the <service> <release> option catalog`, naming the section and key. An
unknown section is rejected with `no such section in the <service> <release>
option catalog (sections registered by a loaded plugin must be declared via
spec.plugins)`.

Three classes of section or key are exempt from the catalog check. A section
declared by a `spec.plugins` entry's `configSection` is trusted, because the
plugin owns it and its options are not in the base catalog. Every key in the
operator-ownership registry above is exempt too, since the operator already
governs those. For Glance, the reserved store sections `os_glance_staging_store`
and `os_glance_tasks_store` are additionally allowed.

The check fails open when it cannot reason about a release. A digest-pinned
Keystone image (or any tag that does not name a release) carries no release to
look up, so the option check is skipped and the admission response returns a
warning. A release newer than the operator build, one with no embedded catalog,
is skipped the same way. A deprecated option that the service still accepts is
admitted with a warning naming its replacement, for example `[DEFAULT] logfile`
superseded by `[DEFAULT] log_file`.

This check lives only in the webhook. There is no CEL or CRD-schema backstop, so
a cluster with the validating webhook disabled accepts a misspelled option name
and surfaces it only at render time. Updates re-run the check only when
`spec.extraConfig`, the plugin section list, or the release field changes.

The operator still does not validate the values in these sections, but a
misspelled option name is now rejected at apply time. A wrong value on a real
option becomes a silent no-op at best and a crash loop at worst, so test changes
in a lab before rolling out. A change to `extraConfig` triggers a ConfigMap
rehash and a rolling Deployment update.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/glance/glance-crd.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ stores are **not** part of this spec — they attach out-of-band through
| `policyOverrides` | `*PolicySpec` | no | Custom oslo.policy rules. A CEL rule requires at least one of `rules` or `configMapRef`; when set, the operator renders a `policy.yaml` and wires `oslo_policy.policy_file` |
| `middleware` | `[]MiddlewareSpec` | no | WSGI middleware filters injected into the `api-paste.ini` pipeline |
| `plugins` | `[]PluginSpec` | no | Service plugins/drivers, modeled as a list-map keyed by `configSection` so duplicate sections are rejected structurally |
| `extraConfig` | `map[string]map[string]string` | no | Free-form INI sections for configuration not covered by explicit fields — the escape hatch for import/staging tuning. The render-time merge follows `plugins < operator defaults < extraConfig` (each stage merged key-wise), so user values win over both. Overrides of operator-owned keys are honored but reported (report-only) via the `ExtraConfigHealthy` condition and an `ExtraConfigOwnedKeyOverride` Warning event — except `[keystone_authtoken] password`, which the validating webhook rejects at admission so the env-injected service password never leaks into the namespace-readable ConfigMap |
| `extraConfig` | `map[string]map[string]string` | no | Free-form INI sections for configuration not covered by explicit fields — the escape hatch for import/staging tuning. The render-time merge follows `plugins < operator defaults < extraConfig` (each stage merged key-wise), so user values win over both. Overrides of operator-owned keys are honored but reported (report-only) via the `ExtraConfigHealthy` condition and an `ExtraConfigOwnedKeyOverride` Warning event — except `[keystone_authtoken] password`, which the validating webhook rejects at admission so the env-injected service password never leaks into the namespace-readable ConfigMap. Option names are validated at admission against a per-release option catalog embedded in the operator (release derived from `spec.openStackRelease`; a release with no embedded catalog skips the check with an admission warning). Sections declared by `spec.plugins`, operator-owned keys, and the reserved store sections `os_glance_staging_store` and `os_glance_tasks_store` are exempt. An unknown section or option is rejected with the section and key named, so an arbitrary backend-named section is refused; backend options belong to the [`GlanceBackend`](./glance-backend-crd.md) CR. A deprecated-but-accepted option is admitted with a warning naming its replacement. Values are never checked, and the rule is webhook-only with no CEL backstop |

### ServiceUserSpec

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/keystone/keystone-crd.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ status:
| `gateway` | [`*GatewaySpec`](#gatewayspec) | No | `nil` | Gateway API HTTPRoute configuration. When set, an HTTPRoute is created targeting the `{name}` Service on port 5000 and attached to the referenced pre-existing Gateway; `status.endpoint` is updated to `https://{hostname}/v3`. When removed, the HTTPRoute is deleted and `status.endpoint` reverts to the cluster-local Service URL. |
| `uwsgi` | [`*UWSGISpec`](#uwsgispec) | No | `nil` | uWSGI application server parameters. When set, the operator uses these values for the Deployment container command. When `nil`, hardcoded defaults (processes=2, threads=1, httpKeepAlive=true) are used in the reconciler. |
| `logging` | [`*LoggingSpec`](#loggingspec) | No | See below | oslo.log configuration for the Keystone API container. When `nil`, the defaulting webhook materializes a baseline (`format=text`, `level=INFO`, `debug=false`, no per-logger overrides) so downstream reconciler code never sees a nil pointer. When set, zero-valued sub-fields are partially filled with the same baseline. |
| `extraConfig` | `map[string]map[string]string` | No | `nil` | Free-form INI sections for additional configuration. The render-time merge follows `plugins < operator defaults < extraConfig` (each stage merged key-wise), so `extraConfig` wins over both. Overrides of operator-owned keys are honored but reported via the `ExtraConfigHealthy` condition and an `ExtraConfigOwnedKeyOverride` Warning event. |
| `extraConfig` | `map[string]map[string]string` | No | `nil` | Free-form INI sections for additional configuration. The render-time merge follows `plugins < operator defaults < extraConfig` (each stage merged key-wise), so `extraConfig` wins over both. Overrides of operator-owned keys are honored but reported via the `ExtraConfigHealthy` condition and an `ExtraConfigOwnedKeyOverride` Warning event. Option names are validated at admission against a per-release option catalog embedded in the operator (release derived from `spec.image.tag`; a digest-pinned image or a release with no embedded catalog skips the check with an admission warning). Sections declared by `spec.plugins` and operator-owned keys are exempt; an unknown section or option is rejected with the section and key named, and a deprecated-but-accepted option is admitted with a warning naming its replacement. Values are never checked, and the rule is webhook-only with no CEL backstop. |

### DeploymentSpec

Expand Down
Loading
Loading