Skip to content

Tempest Test Infrastructure

Reference documentation for the Tempest API test infrastructure. This covers the Tempest container image, version management, per-service test configuration, the image verification script, local execution via hack/run-tempest.sh, the make tempest-test target, and CI integration in both ci.yaml and build-images.yaml. The infrastructure supports multiple releases: the tempest job in ci.yaml uses a release matrix to validate each OpenStack release independently, and build-images.yaml dynamically discovers releases for the Tempest image pipeline.

File Locations

FilePurpose
images/tempest/DockerfileTwo-stage Tempest container image (venv-builder → python-base)
releases/<release>/test-refs.yamlPyPI version pins for test tooling (single source of truth), per release
tests/tempest/keystone-2025-2/Keystone 2025.2 Tempest configuration (tempest.conf, include-tests.txt, exclude-tests.txt)
tests/tempest/keystone-2026-1/Keystone 2026.1 Tempest configuration
tests/tempest/glance-2025-2/Glance 2025.2 Tempest configuration: the tempest.conf / include-tests.txt / exclude-tests.txt triplet, a 00-keystone-cr.yaml identity CR named keystone-glance-tempest-2025-2, and four extra fixtures the CI job applies — 01-catalog-setup-job.yaml (image-catalog bootstrap Job), 02-glance-cr.yaml (Glance CR), 03-glancebackend-cr.yaml (default GlanceBackend CR), 04-glancebackend2-cr.yaml (second, non-default store so the copy-image import test has a copy target)
tests/tempest/glance-2026-1/Glance 2026.1 Tempest configuration (same file set; identity CR keystone-glance-tempest-2026-1)
tests/tempest/barbican-2025-2/Barbican 2025.2 Tempest configuration: the tempest.conf / include-tests.txt / exclude-tests.txt triplet, a 00-keystone-cr.yaml identity CR named keystone-barbican-tempest-2025-2, and three extra fixtures the CI job applies (01-catalog-setup-job.yaml, the key-manager catalog bootstrap Job; 02-barbican-cr.yaml, the Barbican CR; 03-barbicansecretstore-cr.yaml, the default BarbicanSecretStore the job waits on before the Barbican CR)
tests/tempest/barbican-2026-1/Barbican 2026.1 Tempest configuration (same file set; identity CR keystone-barbican-tempest-2026-1)
tests/container-images/verify_tempest.shImage verification script (PASS/FAIL counters)
hack/run-tempest.shLocal orchestration script for running Tempest against a kind cluster
hack/ci-run-tempest.shCI-specific Tempest wrapper with port-forwarding and config generation
hack/ci-generate-tempest-matrix.shGenerates the tempest job matrix from releases/*/, emitting one leg per service (keystone, glance, barbican) per release
hack/tempest/extract-failed.pyPrint anchored regex patterns for failed testcases in a JUnit report (used to build the retry include-list)
hack/tempest/merge-retry-junit.pyMerge a retry subunit stream into a JUnit report, rewriting resolved failures as flakes
hack/tempest/run-tests.shShared in-container runner invoked by both runners; holds the phase + retry + exit-code logic so it stays identical between CI and local runs
Makefiletempest-test target delegates to hack/run-tempest.sh
.github/workflows/ci.yamltempest job with release matrix
.github/workflows/build-images.yamlbuild-tempest and merge-tempest-image jobs (release-parameterized via generate-matrix)

Architecture

text
releases/<release>/test-refs.yaml       Version pins (tempest, barbican-tempest-plugin, keystone-tempest-plugin)

        ▼ (yq resolution)
images/tempest/Dockerfile               2-stage build: venv-builder → python-base

        ├──▶ build-images.yaml          Build, scan, sign, push to GHCR
        │       generate-matrix job      Discovers releases from releases/*/
        │       build-tempest job        Per-release × per-platform builds (amd64, arm64)
        │       merge-tempest-image job  Per-release multi-arch manifest + SBOM + Grype + cosign

        └──▶ ci.yaml                    Build locally, run tests per release
                tempest job (matrix)      Per-release: build image → run Tempest → upload JUnit

tests/tempest/<config-dir>/              │ mounted into container (per-release config)
  tempest.conf                    ──────▶│
  include-tests.txt               ──────▶│
  exclude-tests.txt               ──────▶│

                              _output/tempest/tempest-results.xml (JUnit XML artifact)

Version Management

test-refs.yaml

Location: releases/<release>/test-refs.yaml

Maps each test tool to a PyPI version pin. This file is the single source of truth for what version of each test tool is installed in the Tempest container image. It is separate from source-refs.yaml (which tracks git refs for OpenStack services) so that test tooling versions can evolve independently.

Format:

yaml
tempest: "46.3.0"
keystone-tempest-plugin: "0.21.0"

The tempest pin may run ahead of the release line a directory represents: tempest is branchless (released tags never receive fixes) and upstream validates current plus recent stable releases with the latest tag. Both release directories currently pin tempest 46.3.0 — the glance legs need >= 46.2.0, where the web-download bad-URL negative test accepts the synchronous 400 that glance's DNS-based URI filtering returns.

Each key is a PyPI package name. Values are quoted strings representing exact version pins. CI workflows resolve versions from this file via yq:

bash
TEMPEST_VERSION=$(yq -r '.tempest' releases/<release>/test-refs.yaml)
KTP_VERSION=$(yq -r '.["keystone-tempest-plugin"]' releases/<release>/test-refs.yaml)

Both ci.yaml and build-images.yaml use the same resolution pattern. A null or empty result from yq causes the CI step to fail with a descriptive error.

To update versions, edit test-refs.yaml — no Dockerfile changes are needed.

Container Image

Dockerfile

Location: images/tempest/Dockerfile

The Tempest image uses the same two-stage build pattern as service images but differs in three ways: (1) it installs from PyPI instead of mounting a git source tree, (2) it has no WSGI entrypoint, and (3) it requires no PIP_EXTRAS or EXTRA_APT_PACKAGES.

Stage 1 (build) — extends venv-builder:

  • Declares ARG TEMPEST_VERSION, ARG BARBICAN_TEMPEST_PLUGIN_VERSION, and ARG KEYSTONE_TEMPEST_PLUGIN_VERSION for build-time version injection (resolved from test-refs.yaml by CI)
  • Mounts upper-constraints.txt from the release directory via named build context
  • Installs eight packages into the shared virtualenv via uv pip install --constraint:
PackagePurpose
tempestOpenStack Tempest testing framework
barbican-tempest-pluginBarbican-specific Tempest test plugins; pinned per release (4.4.0 for 2025.2, 4.5.0 for 2026.1)
keystone-tempest-pluginKeystone-specific Tempest test plugins
python-openstackclientopenstack CLI, used by the full-chain ControlPlane E2E verify Job (token issue / catalog list); version pinned by upper-constraints.txt
osc-placementopenstack CLI placement plugin, used by the full-chain ControlPlane E2E placement round-trip (resource class list); version pinned by upper-constraints.txt
python-subunitSubunit test result streaming protocol
junitxmlSubunit-to-JUnit XML conversion (subunit2junitxml)
defusedxmlHardened XML parsing for the result-processing tooling

Stage 2 (runtime) — extends python-base:

  • Copies /var/lib/openstack virtualenv from the build stage via COPY --from=build --link
  • Sets static OCI labels (title, description, licenses, vendor) following the two-layer annotation pattern
  • Runs as openstack user (UID 42424, GID 42424)

Final image properties:

PropertyValue
Useropenstack (UID 42424)
tempest CLIAvailable via PATH (/var/lib/openstack/bin/tempest)
openstack CLIAvailable via PATH; verified by verify_tempest.sh (openstack --version)
subunit2junitxmlAvailable via PATH
Build toolsAbsent (gcc, python3-dev, uv are not in the final image)

Named build contexts:

Context nameContentsMounted as
python-baseRuntime base imageFROM python-base
venv-builderBuild stage base imageFROM venv-builder AS build
upper-constraintsRelease directory containing upper-constraints.txt/tmp/upper-constraints.txt

Build args:

ArgDefaultSource
TEMPEST_VERSION46.3.0test-refs.yaml.tempest
BARBICAN_TEMPEST_PLUGIN_VERSION4.4.0test-refs.yaml.["barbican-tempest-plugin"]
KEYSTONE_TEMPEST_PLUGIN_VERSION0.19.0test-refs.yaml.["keystone-tempest-plugin"]

Local Build

Build the Tempest image locally (requires python-base and venv-builder images):

bash
# Build base images first (if not already available)
docker build images/python-base -t python-base
docker build images/venv-builder -t venv-builder

# Build Tempest image for a specific release (e.g., 2025.2 or 2026.1).
# Resolve versions from the release's test-refs.yaml:
RELEASE=2025.2   # or 2026.1
TEMPEST_VERSION=$(yq -r '.tempest' releases/${RELEASE}/test-refs.yaml)
BTP_VERSION=$(yq -r '.["barbican-tempest-plugin"]' releases/${RELEASE}/test-refs.yaml)
KTP_VERSION=$(yq -r '.["keystone-tempest-plugin"]' releases/${RELEASE}/test-refs.yaml)

docker build images/tempest \
  -t c5c3/tempest:${RELEASE} \
  --build-arg TEMPEST_VERSION=${TEMPEST_VERSION} \
  --build-arg BARBICAN_TEMPEST_PLUGIN_VERSION=${BTP_VERSION} \
  --build-arg KEYSTONE_TEMPEST_PLUGIN_VERSION=${KTP_VERSION} \
  --build-context python-base=docker-image://python-base \
  --build-context venv-builder=docker-image://venv-builder \
  --build-context upper-constraints=releases/${RELEASE}/

Test Configuration

Per-service Tempest configuration lives under tests/tempest/<service>/. Each service directory contains three files: a Tempest configuration file, an include list, and an exclude list.

tempest.conf

Location: tests/tempest/<service>/tempest.conf

INI-format configuration file for the Tempest testing framework. Key sections for the Keystone service:

SectionKeyValuePurpose
[DEFAULT]log_dir/tmp/tempest-logsLog output directory inside container
[identity]uri_v3http://keystone-tempest-api.openstack.svc:5000/v3Keystone v3 API endpoint (in-cluster DNS)
[auth]use_dynamic_credentialsfalseUse static admin credentials (no tenant creation)
[auth]admin_usernameadminAdmin user for API authentication
[auth]admin_password${KEYSTONE_ADMIN_PASSWORD}Injected at runtime from K8s secret
[auth]admin_project_nameadminAdmin project scope
[auth]admin_domain_nameDefaultAdmin domain scope
[identity-feature-enabled]api_v3trueEnable v3 identity API tests
[service_available]identitytrueIdentity service is deployed
[service_available]computefalseNova is not deployed
[service_available]networkfalseNeutron is not deployed
[service_available]volumefalseCinder is not deployed
[service_available]imagefalseGlance is not deployed
[service_available]object-storagefalseSwift is not deployed

The admin_password placeholder ${KEYSTONE_ADMIN_PASSWORD} is resolved at runtime:

  • Local execution: hack/run-tempest.sh extracts it from the keystone-admin K8s secret and passes it as an environment variable to the container
  • CI execution: ci.yaml extracts it via kubectl get secret and substitutes it into a generated copy of tempest.conf using sed

include-tests.txt

Location: tests/tempest/<service>/include-tests.txt

One regex pattern per line. Lines starting with # are comments. Blank lines are ignored. Patterns are matched against Tempest test IDs.

For Keystone, two patterns include all identity-related tests:

PatternMatches
tempest.api.identityCore Tempest identity API tests
keystone_tempest_plugin.testsKeystone-specific plugin tests

Scope-split invariant

Both runners (hack/ci-run-tempest.sh and hack/run-tempest.sh) split this include list into two phase files at runtime and run stestr twice in the same workspace:

  1. phase-1-core.txt — every non-comment line that starts with tempest.
  2. phase-2-plugin.txt — every non-comment line matching ^[a-z0-9_]+_tempest_plugin\., which covers keystone_tempest_plugin., barbican_tempest_plugin., and any plugin added later without naming it in the runners

The two phases run sequentially; within each phase stestr runs at TEMPEST_CONCURRENCY (default 4).

Each runner enforces that every non-comment, non-empty line in include-tests.txt lands in exactly one phase. A line with any other prefix causes the runner to abort with a clear error — this guards against silent drops when new include patterns are added.

Either phase may be empty, and both runners treat that case the same way. The glance legs ship a pure core-tempest include list (tempest.api.image only), which leaves phase-2-plugin.txt empty; the barbican legs ship a pure plugin list (barbican_tempest_plugin.tests.api only), which leaves phase-1-core.txt empty. hack/tempest/run-tests.sh skips whichever phase file is empty and writes an empty subunit stream in its place so the concatenations still line up, instead of running stestr against an empty include list. Only a split that leaves both phases empty is a hard error, in hack/ci-run-tempest.sh and hack/run-tempest.sh alike: an include list with no non-comment pattern at all lands there and exits 1.

Why sequential. Keystone re-resolves the list of enabled federation service providers on every POST /v3/auth/tokens and GET /v3/auth/tokens call and injects it into the response body; the token itself does not cache it. tempest.api.identity.v3.test_tokens.TokensV3Test.test_validate_token issues a token via POST, validates it via GET, and asserts the two response bodies are equal. When keystone_tempest_plugin.tests.api.identity.v3. test_service_providers.ServiceProvidersTest runs concurrently on another stestr worker, its per-test addCleanup deletes the service provider it created — and if that cleanup lands in the ~20 ms window between test_validate_token's POST and GET, the two responses diverge on the service_providers key and the assertion fails.

Upstream's openstack/keystone keystone-tempest gate job sets tempest_test_regex: "keystone_tempest_plugin" and therefore only runs keystone_tempest_plugin.* tests — the core tempest.api.identity.* suite (which contains test_validate_token) never runs in the same Tempest invocation as the service-providers tests, so upstream never observes this race. We run both suites for fuller coverage and replicate the isolation by running them in two separate stestr invocations.

The two phases each emit a subunit stream (phase-1-core.subunit, phase-2-plugin.subunit); the runner concatenates them into tempest.subunit and converts that to JUnit XML. Subunit v2 is stream-concatenation safe by design.

Serial retry of failing tests

After both phases, the runner inspects the JUnit report: if any test is marked failed or errored, those test IDs are extracted and rerun once in a third stestr run invocation with --concurrency 1. The retry output is written to retry.subunit, appended to the combined subunit stream, and merged into the JUnit report: tests that pass on retry have their <failure>/<error> children removed, the enclosing <testsuite> counters are decremented, and a <system-out> note records flaky: failed on first run, passed on retry. Tests that still fail after retry stay as failures.

The two helpers live at hack/tempest/extract-failed.py (reads the JUnit report, prints anchored regex patterns for each failed classname.method) and hack/tempest/merge-retry-junit.py (rewrites the JUnit report from the retry subunit stream). The phase + retry + exit-code sequence itself lives in hack/tempest/run-tests.sh, which is invoked inside the container by both hack/ci-run-tempest.sh and hack/run-tempest.sh. All three files are staged next to tempest.conf so they are available at /etc/tempest/ inside the container. A failure inside either Python helper (missing dependency, parse error) is caught by the runner and falls back to the original stestr exit code rather than aborting the whole Tempest run.

The final exit code is derived from the (possibly retry-adjusted) JUnit report: any remaining failures or errors fail the job. If the retry resolved every failure the runner exits 0. If the initial stestr process crashed hard enough that no JUnit report was produced, the runner still exits non-zero via the captured phase exit code.

exclude-tests.txt

Location: tests/tempest/<service>/exclude-tests.txt

Same format as include-tests.txt. Patterns exclude tests that require services or infrastructure not available in the CI kind cluster:

PatternReason for exclusion
keystone_tempest_plugin\.tests\..*ldapRequires a live LDAP server
keystone_tempest_plugin\.tests\..*federationRequires an external IdP (SAML2/OAuth2)
keystone_tempest_plugin\.tests\..*oauth2Requires an external authorization server

Tracking version-coupled RBAC excludes

Unlike the infrastructure excludes above, the RBAC excludes are coupled to a specific keystone-tempest-plugin version — they exist because the plugin's expected status codes disagree with Keystone's default policies for that release. They must not silently outlive their cause. Each RBAC exclude group therefore carries a # tracked-by: / # re-evaluate-on: comment pair:

text
# tracked-by: <issue URL>
# re-evaluate-on: keystone-tempest-plugin > <current pinned version>

On every keystone-tempest-plugin bump, re-run the excluded RBAC groups against the new plugin and drop any pattern upstream has fixed. The re-evaluate-on version is per-release (> 0.19.0 for 2025.2, > 0.20.0 for 2026.1), matching the plugin pinned in that release's test-refs.yaml.

Adding a New Service

Tempest coverage is keyed per service × release. glance is the worked example: hack/ci-generate-tempest-matrix.sh emits a keystone, a glance, and a barbican leg for every release, so a new service needs one config directory per release, not a single directory. To add another service:

  1. Create a tests/tempest/<service>-<slug>/ directory for each release (e.g. <service>-2025-2 and <service>-2026-1), each with tempest.conf, include-tests.txt, exclude-tests.txt, and the 00-keystone-cr.yaml identity CR the job waits on. The tempest job hard-fails if a release is missing the directory. A service with its own operator payload also ships the CRs the job applies before Tempest runs — glance carries 01-catalog-setup-job.yaml (registers the image service + endpoints in Keystone), 02-glance-cr.yaml, 03-glancebackend-cr.yaml, and 04-glancebackend2-cr.yaml (a second, non-default store: the copy-image import test only executes a real copy when there is a store the image is not already in).
  2. Set [service_available] flags to match the deployed services and point [identity] uri_v3 at the leg's identity CR (glance authenticates against its own keystone-glance-tempest-<slug> CR, not the shared keystone leg's CR).
  3. Extend hack/ci-generate-tempest-matrix.sh to emit the new service leg. If the service needs an extra port-forward, thread it through hack/ci-run-tempest.sh the way glance uses the optional GLANCE_K8S_NAME env (9292 forward + /healthcheck poll + host mappings) and barbican the optional BARBICAN_K8S_NAME (the same three steps on 9311), then add the conditional deploy steps to the tempest job in ci.yaml, gated on matrix.service == '<service>'.

The scope-split needs no edit for a new plugin: phase-2-plugin.txt is filled by ^[a-z0-9_]+_tempest_plugin\., which already matches any <service>_tempest_plugin. pattern. A leg that selects only plugin tests, or only core tempest. ones, leaves the other phase empty, which both runners allow.

A service tested through core tempest.api.* alone needs no image change. One that brings its own Tempest plugin adds a build arg and an install line to images/tempest/Dockerfile plus a version pin to every release's test-refs.yaml, the way barbican-tempest-plugin is wired. hack/run-tempest.sh (local execution) runs a single keystone leg and accepts SERVICE, RELEASE, and ADMIN_SECRET overrides.

Image Verification

Location: tests/container-images/verify_tempest.sh

Validates that the built Tempest container image meets requirements. Uses the same PASS/FAIL counter pattern as other verify_*.sh scripts and sources tests/lib/assertions.sh for assertion helpers.

Usage:

bash
bash tests/container-images/verify_tempest.sh [image_name]
# Default image: c5c3/tempest:46.3.0

Test cases:

TestAssertionValidates
test_tempest_versiontempest --version exits 0 with non-empty outputTempest CLI is installed and functional
test_keystone_tempest_plugin_importablepython3 -c 'import keystone_tempest_plugin' exits 0Plugin is installed in the virtualenv
test_barbican_tempest_plugin_importablepython3 -c 'import barbican_tempest_plugin' exits 0Plugin is installed in the virtualenv
test_subunit2junitxml_availablewhich subunit2junitxml exits 0 with non-empty pathJUnit XML converter is on PATH
test_runs_as_openstack_userwhoami outputs openstackContainer runs as non-root user
test_no_build_tools_in_final_imagewhich gcc, dpkg -s python3-dev, which uv all failBuild tools are not in the runtime image

All test functions use the exit-code guard pattern (|| exit_code=$?) to prevent set -e from aborting the script before assertions run.

In CI, this script runs during the build-tempest job on pull requests to catch image build regressions independently of the full E2E pipeline.

Local Execution

hack/run-tempest.sh

Location: hack/run-tempest.sh

Orchestration script for running Tempest API tests against a deployed OpenStack service in a local kind cluster. Follows the infrastructure deployment script pattern: set -euo pipefail, log() with ISO 8601 timestamps, SCRIPT_DIR/REPO_ROOT resolution, and configurable variables.

Usage:

bash
SERVICE=keystone hack/run-tempest.sh

Environment variables:

VariableDefaultDescription
SERVICE(required)OpenStack service to test (e.g., keystone)
RELEASE2025.2Release version (selects test-refs.yaml and upper-constraints.txt)
TEMPEST_IMAGEc5c3/tempest:localDocker image name for the Tempest container
OUTPUT_DIR_output/tempestDirectory for test results (JUnit XML, subunit stream)
TEMPEST_TIMEOUT1800Timeout for Tempest execution in seconds
NAMESPACEopenstackKubernetes namespace for the service under test

Execution steps:

StepDescriptionFailure behavior
Pre-flight checksValidates SERVICE is set, required tools (docker, kubectl, yq) are installed, Docker is running, service config directory exists, test-refs.yaml existsExits 1 with descriptive error
Build Tempest imageResolves versions from test-refs.yaml, builds with docker build using named build contexts pointing to GHCR base imagesExits on build failure (set -e)
Extract admin passwordReads keystone-admin secret from the K8s cluster via kubectl get secretExits 1 if secret is not found or empty
Run TempestMounts tempest.conf, phases/phase-1-core.txt, phases/phase-2-plugin.txt, exclude-tests.txt, extract-failed.py, merge-retry-junit.py into container; initializes Tempest workspace; runs stestr run --subunit once per phase; concatenates the subunit streams; converts to JUnit XML; reruns any failed tests serially; merges the retry outcome into the JUnit reportNon-zero if any failure remains after retry, or on hard stestr crash

Output files:

FileFormatDescription
_output/tempest/tempest-results.xmlJUnit XMLTest results for CI artifact upload (retry-adjusted)
_output/tempest/tempest.subunitSubunit v2Raw test result stream (phase 1 + phase 2 + retry)
_output/tempest/retry.subunitSubunit v2Retry stream (only present if any tests failed on first run)

The script exits non-zero if the retry-adjusted JUnit report still lists failures or errors, and otherwise exits zero. Both phases always run, so a failure in phase 1 does not short-circuit phase 2. If the initial stestr invocations crashed hard enough that no JUnit report was produced, the captured phase exit code is used as a fallback so infra failures are still reported.

make tempest-test

Location: Makefile

bash
make tempest-test SERVICE=keystone

The tempest-test target validates that SERVICE is set (using the $(if) guard pattern consistent with other Makefile targets) and delegates to hack/run-tempest.sh. Omitting SERVICE produces an error message:

*** tempest-test requires SERVICE, e.g. make tempest-test SERVICE=keystone.  Stop.

CI Integration

ci.yaml — tempest Job

The tempest job is a dedicated job that deploys services into a kind cluster and runs the OpenStack Tempest test suite. hack/ci-generate-tempest-matrix.sh fans the matrix out over two dimensions — a release and a service — so each OpenStack release is validated independently for keystone, glance, and barbican, each leg with its own Tempest configuration and identity CR (and, for glance, an image CR and backend; for barbican, a key-manager CR and its secret store).

Release matrix:

The generator scans releases/*/ and emits one keystone, one glance, and one barbican leg per release; each service requires a matching tests/tempest/<service>-<slug> directory or the job hard-fails. service-k8s-name always equals cr-name (the Keystone identity CR the job waits on and port-forwards on 5000). The glance legs additionally carry glance-cr-name (the Glance CR the job waits on and port-forwards on 9292) and the barbican legs barbican-cr-name (the Barbican CR, waited on and port-forwarded on 9311).

ServiceReleaseConfig directoryKeystone CR (cr-name = service-k8s-name)Glance CR (glance-cr-name)Barbican CR (barbican-cr-name)
keystone2025.2tests/tempest/keystone-2025-2keystone-tempest-2025-2
keystone2026.1tests/tempest/keystone-2026-1keystone-tempest-2026-1
glance2025.2tests/tempest/glance-2025-2keystone-glance-tempest-2025-2glance-tempest-2025-2
glance2026.1tests/tempest/glance-2026-1keystone-glance-tempest-2026-1glance-tempest-2026-1
barbican2025.2tests/tempest/barbican-2025-2keystone-barbican-tempest-2025-2barbican-tempest-2025-2
barbican2026.1tests/tempest/barbican-2026-1keystone-barbican-tempest-2026-1barbican-tempest-2026-1

Step sequence:

StepDescription
Build service imagehack/ci-build-service-image.sh with RELEASE=matrix.release
Build Tempest imagehack/ci-build-tempest-image.sh with RELEASE=matrix.release, image tagged c5c3/tempest:<release>
Load images into kindLoads operator and release-specific service images
Deploy Glance operator (glance leg only)hack/ci-deploy-operator.sh with OPERATOR=glance, NAMESPACE=glance-system (before the Keystone CR reconciles)
Deploy Barbican operator (barbican leg only)hack/ci-deploy-operator.sh with OPERATOR=barbican, NAMESPACE=barbican-system, and BARBICAN_SECRET_STORE_GRANTS=openstack=openbao-instance-provisioner (the managed store lives in openstack, and the operator mints its provisioner token only where the chart granted it a TokenRequest Role — restricted to that one account, because openstack also hosts the accounts that read credentials out of OpenBao)
Deploy Keystone CRApplies matrix.config-dir/00-keystone-cr.yaml, waits for matrix.cr-name Ready
Bootstrap image catalog (glance leg only)Applies matrix.config-dir/01-catalog-setup-job.yaml, waits for the glance-tempest-catalog-setup Job to complete (registers the image service + endpoints in Keystone that the Glance CR needs to reconcile)
Deploy Glance CR (glance leg only)Applies matrix.config-dir/02-glance-cr.yaml, 03-glancebackend-cr.yaml, and 04-glancebackend2-cr.yaml, waits for matrix.glance-cr-name Ready
Bootstrap key-manager catalog (barbican leg only)Applies matrix.config-dir/01-catalog-setup-job.yaml, waits for the barbican-tempest-catalog-setup Job to complete (registers the key-manager service + endpoints in Keystone that the Barbican CR needs to reconcile, plus the key-manager:service-admin role the plugin's quota tests assign via dynamic credentials)
Deploy Barbican CR (barbican leg only)Applies matrix.config-dir/02-barbican-cr.yaml and 03-barbicansecretstore-cr.yaml, waits for barbicansecretstore/<barbican-cr-name>-store Ready first, then for matrix.barbican-cr-name Ready. Barbican renders no config and never starts without a credential-ready default store, so the separate wait keeps a store-side failure from surfacing as an opaque Barbican timeout
Run Tempest API testshack/ci-run-tempest.sh with CONFIG_DIR, TEMPEST_IMAGE, and SERVICE_K8S_NAME from matrix; the glance leg also passes GLANCE_K8S_NAME and the barbican leg BARBICAN_K8S_NAME (each empty on the other legs, which disables the matching 9292 or 9311 port-forward)
Upload Tempest resultsUploads _output/tempest/ (minus the rendered tempest.conf, which carries the substituted admin password) as artifact with 14-day retention

CI-specific adaptations (compared to local execution):

AspectLocal (hack/run-tempest.sh)CI (hack/ci-run-tempest.sh)
Service endpointIn-cluster DNS (<service-k8s-name>.openstack.svc:5000)Port-forwarded to localhost:5000
Glance endpointNot supported (local runs are keystone-only)When GLANCE_K8S_NAME is set (glance legs), additionally forwards svc/<glance-cr-name> on 9292, polls its /healthcheck, and add-hosts its cluster DNS names to 127.0.0.1 so the catalog's image endpoint resolves to the forwarded port
Barbican endpointNot supported (local runs are keystone-only)When BARBICAN_K8S_NAME is set (barbican legs), additionally forwards svc/<barbican-cr-name> on 9311, polls its /healthcheck, and add-hosts its cluster DNS names to 127.0.0.1 so the catalog's key-manager endpoint resolves to the forwarded port
Credential injectionEnvironment variable passed to containersed substitution into generated config copy
Base imagesPulled from GHCR (docker-image://ghcr.io/...)Built locally in prior CI steps (no --build-context for bases)
Artifact uploadManual inspection of _output/actions/upload-artifact with tempest-<service>-<release>-results name

Artifact name: tempest-<service>-<release>-results (e.g., tempest-keystone-2025.2-results, tempest-glance-2026.1-results) Retention: 14 days

build-images.yaml — build-tempest Job

Builds the Tempest container image per release and per platform, runs verification on PRs, and pushes by digest on push events. The job is parameterized by release via the generate-matrix job, which discovers all releases from releases/*/ directories.

Dependencies: needs: [lint-dockerfiles, merge-base-images, verify-base-images, generate-matrix]

Matrix strategy: release × platform × runner (from generate-matrix.tempest-matrix; ARM64 excluded on PRs)

Step sequence:

StepConditionDescription
Resolve Tempest versionsAlwaysReads releases/<release>/test-refs.yaml via yq
Generate metadataAlwaysOCI labels via docker/metadata-action
Build Tempest imagePR: amd64 only; push: both platformsdocker/build-push-action with named build contexts, upper-constraints from releases/<release>/
Export digestPush onlyWrites digest to /tmp/digests/ for merge job
Upload digest artifactPush onlydigests-tempest-<release>-<platform-pair>, 1-day retention
Scan for vulnerabilitiesPR, amd64 onlyGrype scan against loaded image
Upload SARIFPR, if scan produced outputGitHub Security tab under grype-tempest-<release>-<platform>
Verify Tempest imagePR, amd64 onlyRuns verify_tempest.sh against the built image

PR vs push behavior:

BehaviorPull RequestPush to main/stable
Platforms builtamd64 onlyamd64 + arm64
Image destinationLoaded locally (load: true)Pushed by digest to GHCR
Verificationverify_tempest.sh runsDeferred to merge-tempest-image
Vulnerability scanAgainst local imageAgainst SBOM (in merge job)

build-images.yaml — merge-tempest-image Job

Assembles per-platform digests into a multi-arch manifest list with full supply chain security. The job is parameterized by release via the generate-matrix job.

Dependencies: needs: [build-tempest, generate-matrix]Condition: if: github.event_name != 'pull_request'Matrix strategy: release (from generate-matrix.tempest-release-matrix)

Permissions:

PermissionPurpose
contents: readRepository checkout
packages: writePush manifest list to GHCR
id-token: writeSigstore OIDC signing for cosign and build provenance
attestations: writeGitHub Attestations API
security-events: writeSARIF upload to GitHub Security tab

Image tags applied:

TagExampleDescription
<release>ghcr.io/<owner>/tempest:2025.2Release series tag
<tempest-version>ghcr.io/<owner>/tempest:46.3.0Tempest PyPI version (main branch only)
<release>-<commit-sha>ghcr.io/<owner>/tempest:2025.2-<sha>Release + git commit for traceability

Supply chain security steps:

StepToolOutput
SBOM generationanchore/sbom-actionsbom-tempest-<release>.cyclonedx.json (CycloneDX format)
Vulnerability scananchore/scan-action (Grype)SARIF uploaded to GitHub Security tab (grype-tempest-<release>)
SBOM attestationactions/attestSigned attestation pushed to GHCR registry
Image signingsigstore/cosignKeyless signature via Sigstore OIDC

lint-dockerfiles

The Tempest Dockerfile is included in the lint-dockerfiles matrix job alongside other Dockerfiles:

yaml
strategy:
  matrix:
    dockerfile:
      - images/python-base/Dockerfile
      - images/venv-builder/Dockerfile
      - images/keystone/Dockerfile
      - images/tempest/Dockerfile

Hadolint runs with failure-threshold: warning and the project's .hadolint.yaml configuration. The Tempest Dockerfile uses # hadolint ignore=DL3006 on FROM directives (consistent with other Dockerfiles) because named build contexts resolve the base image at build time.

Data Flow (CI End-to-End)

text
releases/<release>/test-refs.yaml ──yq──▶ TEMPEST_VERSION + BARBICAN_TEMPEST_PLUGIN_VERSION + KEYSTONE_TEMPEST_PLUGIN_VERSION


         docker build --build-arg TEMPEST_VERSION=... \
                      --build-arg BARBICAN_TEMPEST_PLUGIN_VERSION=... \
                      --build-arg KEYSTONE_TEMPEST_PLUGIN_VERSION=... \
                      --build-context upper-constraints=releases/<release>/ \
                      images/tempest/

                         ▼ (tempest job, per matrix.release)
         kubectl port-forward svc/<service-k8s-name> 5000:5000


         hack/ci-run-tempest.sh (resolve localhost URI + admin password)


         docker run --network host \
           -v tempest.conf:/etc/tempest/tempest.conf \
           -v phases/phase-1-core.txt:/etc/tempest/phases/phase-1-core.txt \
           -v phases/phase-2-plugin.txt:/etc/tempest/phases/phase-2-plugin.txt \
           -v exclude-tests.txt:/etc/tempest/exclude-tests.txt \
           -v extract-failed.py:/etc/tempest/extract-failed.py \
           -v merge-retry-junit.py:/etc/tempest/merge-retry-junit.py \
           -v run-tests.sh:/etc/tempest/run-tests.sh \
           c5c3/tempest:<release> bash /etc/tempest/run-tests.sh
         #
         # Internal logic of run-tests.sh (kept here for reference; the runner
         # scripts never invoke these steps inline — they always `bash
         # /etc/tempest/run-tests.sh` so CI and local runs share one code path):
         #
         #   tempest init . && cp tempest.conf etc/
         #   stestr run --include-list phases/phase-1-core.txt   --subunit → /output/phase-1-core.subunit
         #   stestr run --include-list phases/phase-2-plugin.txt --subunit → /output/phase-2-plugin.subunit
         #   cat phase-1-core.subunit phase-2-plugin.subunit > tempest.subunit
         #   subunit2junitxml < tempest.subunit > tempest-results.xml
         #   # retry any failed tests once serially, then rewrite the JUnit:
         #   python3 /etc/tempest/extract-failed.py tempest-results.xml > retry-list.txt
         #   stestr run --include-list retry-list.txt --concurrency 1 --subunit → /output/retry.subunit
         #   cat phase-1-core.subunit phase-2-plugin.subunit retry.subunit > tempest.subunit
         #   python3 /etc/tempest/merge-retry-junit.py tempest-results.xml retry.subunit


         actions/upload-artifact ──▶ tempest-<service>-<release>-results (14-day retention)

Dependencies on Prior Features

ArtifactUsed by Tempest infrastructure
images/python-base/Dockerfile, images/venv-builder/DockerfileBase images for the Tempest Dockerfile build chain
releases/<release>/upper-constraints.txtDependency constraints for PyPI installs
tests/lib/assertions.shAssertion helpers sourced by verify_tempest.sh
SBOM attestation pattern in build-images.yamlReused by merge-tempest-image for Tempest SBOM
Cosign signing pattern in build-images.yamlReused by merge-tempest-image for Tempest signing
Two-layer OCI annotation patternStatic Dockerfile labels + CI metadata-action
Grype scanning pattern in build-images.yamlReused by build-tempest and merge-tempest-image