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
12 changes: 12 additions & 0 deletions containers/horizontest/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See the OWNERS docs at https://www.kubernetes.dev/docs/guide/owners/
approvers:
- mcgonago
- deshipu
- xtmprsqzntwlfb
- HanzJas

reviewers:
- mcgonago
- deshipu
- xtmprsqzntwlfb
- HanzJas
1 change: 1 addition & 0 deletions containers/horizontest/buildrequirements.lock
31 changes: 31 additions & 0 deletions containers/horizontest/buildrequirements.lock.master
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
build==1.5.0
calver==2025.10.20
cython==3.3.0
flit-core==3.12.0
flit-core==4.0.2
hatch-fancy-pypi-readme==25.1.0
hatch-vcs==0.5.0
hatchling==1.32.0
maturin==1.14.1
packaging==26.3
pathspec==1.1.1
pbr==7.0.3
pluggy==1.6.0
poetry-core==2.4.1
pybind11==3.1.0
pyproject-hooks==1.2.0
scikit-build-core==1.0.3
semantic-version==2.10.0
setuptools-rust==1.13.0
setuptools-scm==10.2.1
setuptools-scm==7.1.0
tomlkit==0.15.1
trove-classifiers==2026.6.1.19
typing-extensions==4.16.0
vcs-versioning==2.3.1
wheel==0.48.0

# The following packages are considered to be unsafe in a requirements file:
pip==26.2.1
setuptools==83.0.0
setuptools==84.0.0
71 changes: 71 additions & 0 deletions containers/horizontest/horizontest/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
ARG BASE_IMAGE=localhost/openstack/openstack-base:latest

# --- Build stage: pre-build wheels ---
FROM ${BASE_IMAGE} AS build

ARG CONSTRAINTS_FILE=requirements.lock
COPY ${CONSTRAINTS_FILE} /deps-upper-constraints.txt

ARG PIP_NO_BINARY=""
ENV PIP_NO_BINARY=${PIP_NO_BINARY}
# Required to build Rust-based Python packages from source
ENV MATURIN_NO_INSTALL_RUST=true

COPY horizontest/builddeps.txt /tmp/builddeps.txt
RUN pkgs=$(cat /tmp/builddeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \
if [ -n "${pkgs}" ]; then microdnf -y install ${pkgs} && microdnf clean all; fi

COPY horizontest/pythonbuilddeps.txt /tmp/pythonbuilddeps.txt
RUN pkgs=$(cat /tmp/pythonbuilddeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \
if [ -n "${pkgs}" ]; then pip3 install --no-cache-dir -c /deps-upper-constraints.txt ${pkgs}; fi && \
rm /tmp/pythonbuilddeps.txt

# Build wheels for all packages pinned in the lockfile so the runtime stage
# can install without build tools or network access.
RUN pip3 wheel --no-cache-dir --no-deps \
--wheel-dir=/wheels -r /deps-upper-constraints.txt

# --- Runtime stage ---
FROM ${BASE_IMAGE}

LABEL summary="OpenStack Horizon Selenium test runner" \
io.k8s.description="Runs Horizon's pytest/selenium UI integration tests against a live dashboard (test-operator HorizonTest)"

RUN uid_gid_manage horizontest

COPY horizontest/bindeps.txt /tmp/bindeps.txt
RUN pkgs=$(cat /tmp/bindeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \
if [ -n "${pkgs}" ]; then microdnf -y install ${pkgs} && microdnf clean all; fi && \
rm /tmp/bindeps.txt

COPY --from=build /wheels /wheels
COPY --from=build /deps-upper-constraints.txt /deps-upper-constraints.txt
COPY horizontest/pythondeps.txt /tmp/pythondeps.txt
RUN pkgs=$(cat /tmp/pythondeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \
pip3 install --no-cache-dir --prefix=/usr \
-c /deps-upper-constraints.txt \
--find-links=/wheels \
${pkgs} && \
rm -rf /wheels /tmp/pythondeps.txt

RUN curl -s -L \
https://github.com/mozilla/geckodriver/releases/download/v0.37.1/geckodriver-v0.37.1-linux64.tar.gz \
| tar -zxvf - -C /usr/local/bin/

RUN curl -OL http://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img \
-o /usr/local/share/cirros-0.6.2-x86_64-disk

COPY horizontest/config/etc/sudoers.d/horizontest_sudoers /etc/sudoers.d/horizontest_sudoers
RUN chmod 440 /etc/sudoers.d/horizontest_sudoers

RUN mkdir -p /var/lib/horizontest/external_files && \
chown -R horizontest:horizontest /var/lib/horizontest

COPY horizontest/scripts/run_horizontest.sh /usr/local/bin/run_horizontest.sh
RUN chmod +x /usr/local/bin/run_horizontest.sh

ENV USE_EXTERNAL_FILES=true

USER horizontest

ENTRYPOINT ["/usr/local/bin/run_horizontest.sh"]
16 changes: 16 additions & 0 deletions containers/horizontest/horizontest/bindeps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
python3
python3-pip
firefox
xorg-x11-server-Xvfb
git-core
tar
# Pinned to 43.x to match upper-constraints.txt (cryptography==43.0.3).
# The default el10 RPM ships 49.x which conflicts with pyOpenSSL 24.2.1
# (requires cryptography<44), causing an ImportError at runtime. Bump this
# pin once pyOpenSSL >= 25.0 lands in upper-constraints (drops the <44 cap).
python3-cryptography-43.0.0-4.el10
# Pillow runtime libraries (transitive dep of horizon)
libjpeg-turbo
freetype
libwebp
libtiff
21 changes: 21 additions & 0 deletions containers/horizontest/horizontest/builddeps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
git-core
gcc
gcc-c++
python3-devel
python3-setuptools
libffi-devel
openssl-devel
# Pinned to 43.x to match upper-constraints.txt (cryptography==43.0.3).
# The default el10 RPM ships 49.x which conflicts with pyOpenSSL 24.2.1
# (requires cryptography<44), causing an ImportError at runtime. Bump this
# pin once pyOpenSSL >= 25.0 lands in upper-constraints (drops the <44 cap).
python3-cryptography-43.0.0-4.el10
# Required to build Pillow from source (transitive dep of horizon)
libjpeg-turbo-devel
zlib-devel
freetype-devel
libwebp-devel
libtiff-devel
# To build bcrypt / rpds-py from source
rust
cargo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
horizontest ALL=(ALL) NOPASSWD: ALL
2 changes: 2 additions & 0 deletions containers/horizontest/horizontest/pythonbuilddeps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pbr
wheel
11 changes: 11 additions & 0 deletions containers/horizontest/horizontest/pythondeps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pytest
pytest-django
pytest-html
Django
horizon
selenium
testtools
xvfbwrapper
urllib3
# OpenStack CLI used by run_horizontest.sh (image/project/user/flavor mgmt)
python-openstackclient
219 changes: 219 additions & 0 deletions containers/horizontest/horizontest/scripts/run_horizontest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/bin/sh

set -x

HORIZONTEST_DIR=/var/lib/horizontest
HORIZON_LOGS_DIR_NAME=${HORIZON_LOGS_DIR_NAME:-"horizon"}
IMAGE_URL=http://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img
PROJECT_NAME=horizontest
USER_NAME=horizontest
PASSWORD=horizontest
FLAVOR_NAME=m1.tiny
GROUP_NAME=admins
SELENIUM_MESSAGE_WAIT=120
SELENIUM_EXPLICIT_WAIT=360
SELENIUM_PAGE_TIMEOUT=240
SELENIUM_IMPLICIT_WAIT=30
HORIZONTEST_DEBUG_MODE="${HORIZONTEST_DEBUG_MODE:-false}"
EXTRA_FLAG="${EXTRA_FLAG:-"not pagination and not federation"}"
PROJECT_NAME_XPATH="${PROJECT_NAME_XPATH:-"//span[@class='rcueicon rcueicon-folder-open']/ancestor::li"}"
IMAGE_FILE_NAME=cirros-0.6.2-x86_64-disk
IMAGE_FILE_NAME_WITH_SIZE="cirros-0.6.2-x86_64-disk (20.4 MB)"
IMAGE_FILE="/usr/local/share/${IMAGE_FILE_NAME}"
if [[ ! -f "${IMAGE_FILE}" ]]; then
IMAGE_FILE="/var/lib/horizontest/${IMAGE_FILE_NAME}"
fi
SUBNET_NAME=public_subnet
HELP_SEQUENCE=".//*[normalize-space()='Help']"
HELP_URL="https://docs.redhat.com/en/documentation/red_hat_openstack_services_on_openshift/"
TEST_MATERIAL_THEME=False
USER_NAME_XPATH="//span[@class='rcueicon rcueicon-user']/ancestor::li"
BROWSE_LEFT_PANEL_MAIN="Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Project,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Admin,Identity,Identity,Identity,Identity,Identity"
BROWSE_LEFT_PANEL_SEC="Project,None,None,None,None,None,Volumes,Volumes,Network,Network,Network,Network,Network,Network,Network,Object Store,None,Compute,Compute,Compute,Compute,Compute,Volume,Volume,Volume,Volume,Network,Network,Network,Network,Network,System,System,System,None,None,None,None,None"
BLP_SEC_LINE_XPATH=".//*[@class='navbar primary persistent-secondary']"
BLP_SEC_LINE_REQ_BTN=".//*[@class='navbar primary persistent-secondary']//a[normalize-space()='{sec_panel}']//ancestor::li"
BLP_SIDEBAR_XPATH=".//*[@class='navbar primary persistent-secondary']//a[normalize-space()='{sec_panel}']//ancestor::li//*[@class='dropdown-menu']"
PROJECT_TEXT_XPATH="${PROJECT_TEXT_XPATH:-".//span[@class='rcueicon rcueicon-folder-open']/ancestor::li"}"
SERVICES_REGION_NAME_XPATH=".//li[@id='services_region_switcher']/a"
SERVICES_REGION_BTN_TEXT_PATTERN="Managing Region {region}"
SERVICES_REGION_DROPDOWN_XPATH=".//li[@id='services_region_switcher']"

# assert mandatory variables have been set
if [[ -z "${ADMIN_USERNAME}" ]]; then
ADMIN_USERNAME=$(python3 -c "from openstack.config import OpenStackConfig; print(OpenStackConfig().get_one(cloud='default').auth['username'])")
fi
if [[ -z "${ADMIN_PASSWORD}" ]]; then
ADMIN_PASSWORD=$(python3 -c "from openstack.config import OpenStackConfig; print(OpenStackConfig().get_one(cloud='default').auth['password'])")
fi
[[ -z ${DASHBOARD_URL} ]] && echo "DASHBOARD_URL not set" && exit 1
[[ -z ${AUTH_URL} ]] && echo "AUTH_URL not set" && exit 1
[[ -z ${REPO_URL} ]] && REPO_URL="https://review.opendev.org/openstack/horizon"
[[ -z ${HORIZON_REPO_BRANCH} ]] && HORIZON_REPO_BRANCH="master"

function catch_error_if_debug {
echo "File run_horizontest.sh has run into an error!"
sleep infinity
}

# Catch errors when in debug mode
if [ ${HORIZONTEST_DEBUG_MODE} == true ]; then
trap catch_error_if_debug ERR
fi

#This function is temporarily added until tempest cleanup is implemented
function clean_leftover_images {
openstack image list -c Name -f value --os-cloud default | xargs -I {} openstack image delete {} --os-cloud default
}

function create_custom_resources {
if ! openstack image show --os-cloud default ${IMAGE_FILE_NAME} ; then
if [ ! -f "$IMAGE_FILE" ]; then
curl -o "$IMAGE_FILE" -OL ${IMAGE_URL}
fi
openstack image create \
--os-cloud default \
--disk-format qcow2 \
--container-format bare \
--file "$IMAGE_FILE" --public ${IMAGE_FILE_NAME}
fi

if ! openstack project show --os-cloud default ${PROJECT_NAME} ; then
openstack project create \
--os-cloud default \
--description 'Horizon Selenium test project' ${PROJECT_NAME}
fi

if ! openstack user show --os-cloud default ${USER_NAME}; then
openstack user create \
--os-cloud default \
--project ${PROJECT_NAME} \
--password ${PASSWORD} ${USER_NAME}
fi

openstack role add \
--os-cloud default \
--user ${USER_NAME} \
--project ${PROJECT_NAME} member

if ! openstack flavor show --os-cloud default ${FLAVOR_NAME}; then
openstack flavor create \
--os-cloud default \
--public ${FLAVOR_NAME} \
--ram 512 --disk 1 --vcpus 1
fi

if ! openstack group show --os-cloud default ${GROUP_NAME}; then
openstack group create \
--os-cloud default ${GROUP_NAME}
fi
}

function delete_custom_resources {
if openstack image show --os-cloud default ${IMAGE_FILE_NAME}; then
openstack image delete \
--os-cloud default ${IMAGE_FILE_NAME}
fi

if openstack project show --os-cloud default ${PROJECT_NAME}; then
openstack project delete \
--os-cloud default ${PROJECT_NAME}
fi

if openstack user show --os-cloud default ${USER_NAME}; then
openstack user delete \
--os-cloud default ${USER_NAME}
fi

if openstack flavor show --os-cloud default ${FLAVOR_NAME}; then
openstack flavor delete \
--os-cloud default ${FLAVOR_NAME}
fi

if openstack group show --os-cloud default ${GROUP_NAME}; then
openstack group delete \
--os-cloud default ${GROUP_NAME}
fi
}

pushd ${HORIZONTEST_DIR}
GIT_CMD_ARGS=()
if [[ ${REPO_URL} == *redhat.com* ]]; then
GIT_CMD_ARGS+=(-c http.sslVerify=false)
fi

_trial=0
_limit=5
_delay=30

GIT_CLONE_CMD=(git "${GIT_CMD_ARGS[@]}" clone "${REPO_URL}" "${HORIZONTEST_DIR}/horizon")

until ("${GIT_CLONE_CMD[@]}"); do
if [ "${_trial}" -lt "${_limit}" ]; then
_trial=$(( _trial + 1 ))
echo "Git clone failed; retrying in ${_delay} seconds..."
sleep "${_delay}"
else
exit 1
fi
done
chown -R horizontest:horizontest horizon
pushd horizon
git "${GIT_CMD_ARGS[@]}" pull --rebase
git checkout ${HORIZON_REPO_BRANCH}

clean_leftover_images
create_custom_resources
pushd ${HORIZONTEST_DIR}/horizon/openstack_dashboard/test/integration_tests/

# set variables in horizon.conf
crudini --set horizon.conf dashboard dashboard_url ${DASHBOARD_URL}/dashboard/
crudini --set horizon.conf dashboard auth_url ${AUTH_URL}
crudini --set horizon.conf dashboard help_url ${HELP_URL}
crudini --set horizon.conf identity username ${USER_NAME}
crudini --set horizon.conf identity password ${PASSWORD}
crudini --set horizon.conf identity home_project ${PROJECT_NAME}
crudini --set horizon.conf identity admin_username ${ADMIN_USERNAME}
crudini --set horizon.conf identity admin_password ${ADMIN_PASSWORD}
crudini --set horizon.conf image http_image ${IMAGE_URL}
crudini --set horizon.conf image images_list ${IMAGE_FILE_NAME}
crudini --set horizon.conf launch_instances image_name "${IMAGE_FILE_NAME_WITH_SIZE}"
crudini --set horizon.conf selenium message_wait ${SELENIUM_MESSAGE_WAIT}
crudini --set horizon.conf selenium explicit_wait ${SELENIUM_EXPLICIT_WAIT}
crudini --set horizon.conf selenium page_timeout ${SELENIUM_PAGE_TIMEOUT}
crudini --set horizon.conf selenium implicit_wait ${SELENIUM_IMPLICIT_WAIT}
crudini --set horizon.conf network subnet_name ${SUBNET_NAME}
crudini --set horizon.conf theme project_name_xpath "${PROJECT_NAME_XPATH}"
crudini --set horizon.conf theme help_sequence "${HELP_SEQUENCE}"
crudini --set horizon.conf theme test_material_theme "${TEST_MATERIAL_THEME}"
crudini --set horizon.conf theme user_name_xpath "${USER_NAME_XPATH}"
crudini --set horizon.conf theme browse_left_panel_main "${BROWSE_LEFT_PANEL_MAIN}"
crudini --set horizon.conf theme browse_left_panel_sec "${BROWSE_LEFT_PANEL_SEC}"
crudini --set horizon.conf theme b_l_p_sec_line_xpath "${BLP_SEC_LINE_XPATH}"
crudini --set horizon.conf theme b_l_p_sec_line_req_btn "${BLP_SEC_LINE_REQ_BTN}"
crudini --set horizon.conf theme b_l_p_sidebar_xpath "${BLP_SIDEBAR_XPATH}"
crudini --set horizon.conf theme project_text_xpath "${PROJECT_TEXT_XPATH}"
crudini --set horizon.conf services_regions region_name_xpath "${SERVICES_REGION_NAME_XPATH}"
crudini --set horizon.conf services_regions region_btn_text_pattern "${SERVICES_REGION_BTN_TEXT_PATTERN}"
crudini --set horizon.conf services_regions region_dropdown_xpath "${SERVICES_REGION_DROPDOWN_XPATH}"
popd

# run horizon selenium tests
INTEGRATION_TESTS=1 SELENIUM_HEADLESS=True pytest openstack_dashboard/test/selenium/integration/ -k "${EXTRA_FLAG}" \
--junitxml="test_reports/ui_integration_test_results.xml" \
--html="test_reports/ui_integration_test_results.html" --self-contained-html
RETURN_VALUE=$?

# copy logs to external_files
echo "Copying logs file"
LOG_DIR=${HORIZONTEST_DIR}/external_files/${HORIZON_LOGS_DIR_NAME}/
mkdir -p ${LOG_DIR}
cp -rf ${HORIZONTEST_DIR}/horizon/test_reports/* ${LOG_DIR}

delete_custom_resources

# Keep pod in running state when in debug mode
if [ ${HORIZONTEST_DEBUG_MODE} == true ]; then
sleep infinity
fi

exit ${RETURN_VALUE}
Empty file.
1 change: 1 addition & 0 deletions containers/horizontest/requirements.lock
Loading