-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #169 from larsks/feature/integration-tests
- Loading branch information
Showing
8 changed files
with
351 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.tox | ||
.coverage | ||
.pytest_cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Set update schedule for GitHub Actions | ||
|
||
version: 2 | ||
updates: | ||
|
||
- package-ecosystem: "github-actions" | ||
directory: "/" | ||
schedule: | ||
# Check for updates to GitHub Actions every week | ||
interval: "weekly" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
name: Build container image | ||
|
||
on: | ||
push: | ||
pull_request: | ||
workflow_dispatch: | ||
|
||
env: | ||
# Use docker.io for Docker Hub if empty | ||
REGISTRY: ghcr.io | ||
# github.repository as <account>/<repo> | ||
IMAGE_NAME: ${{ github.repository }} | ||
|
||
jobs: | ||
build: | ||
|
||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: read | ||
packages: write | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.11' | ||
|
||
- name: Install dependencies | ||
run: | | ||
sudo apt -y install podman | ||
pip install tox | ||
- name: Build image for testing | ||
uses: docker/build-push-action@v6 | ||
with: | ||
context: . | ||
file: Containerfile | ||
tags: esi-leap-testing-${{ github.run_id }} | ||
|
||
- name: Run integration tests | ||
env: | ||
ESI_LEAP_IMAGE: esi-leap-testing-${{ github.run_id }} | ||
run: | | ||
tox -e integration | ||
# Login against a Docker registry except on PR | ||
# https://github.com/docker/login-action | ||
- name: Log into registry ${{ env.REGISTRY }} | ||
if: github.event_name != 'pull_request' | ||
uses: docker/login-action@v3 | ||
with: | ||
registry: ${{ env.REGISTRY }} | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
# Extract metadata (tags, labels) for Docker | ||
# https://github.com/docker/metadata-action | ||
- name: Extract Docker metadata | ||
id: meta | ||
uses: docker/metadata-action@v5 | ||
with: | ||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
tags: | | ||
type=semver,pattern=v{{version}} | ||
type=semver,pattern=v{{major}}.{{minor}} | ||
type=semver,pattern=v{{major}} | ||
type=ref,event=branch | ||
type=ref,event=pr | ||
type=sha | ||
# Build and push Docker image with Buildx (don't push on PR) | ||
# https://github.com/docker/build-push-action | ||
- name: Build and push Docker image | ||
uses: docker/build-push-action@v6 | ||
with: | ||
context: . | ||
push: ${{ github.event_name != 'pull_request' && github.ref_name == 'master' }} | ||
tags: ${{ steps.meta.outputs.tags }} | ||
labels: ${{ steps.meta.outputs.labels }} | ||
file: Containerfile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
"""These tests bring up an esi-container and then run tests against the esi-leap API. | ||
Note that the fixtures in this file are session-scoped (rather than | ||
the default method-scoped) in avoid the cost of repeatedly | ||
creating/deleting the container environment. This means that any tests | ||
in this file must be avoid side effects that would impact subsequent | ||
tests. | ||
""" | ||
|
||
import os | ||
import subprocess | ||
import requests | ||
import pytest | ||
import string | ||
import random | ||
import tempfile | ||
import time | ||
import docker | ||
|
||
from pathlib import Path | ||
|
||
esi_leap_config_template = """ | ||
[DEFAULT] | ||
log_dir= | ||
log_file= | ||
transport_url=fake:// | ||
[database] | ||
connection=mysql+pymysql://esi_leap:{mysql_user_password}@{mysql_container}/esi_leap | ||
[oslo_messaging_notifications] | ||
driver=messagingv2 | ||
transport_url=fake:// | ||
[oslo_concurrency] | ||
lock_path={tmp_path}/locks | ||
[dummy_node] | ||
dummy_node_dir={tmp_path}/nodes | ||
[pecan] | ||
auth_enable=false | ||
""" | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def docker_client(): | ||
"""A client for interacting with the Docker API""" | ||
client = docker.from_env() | ||
return client | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def tmp_path(): | ||
"""A session-scoped temporary directory that will be removed when the | ||
session closes.""" | ||
|
||
with tempfile.TemporaryDirectory(prefix="pytest") as tmpdir: | ||
yield Path(tmpdir) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def random_string(): | ||
"""A session-scoped random string that we use to generate names, | ||
credentials, etc. that are unique to the test session.""" | ||
|
||
return "".join(random.sample(string.ascii_lowercase, 8)) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def test_network(docker_client, random_string): | ||
"""Create a Docker network for the test (and clean it up when we're done)""" | ||
|
||
network_name = f"esi-leap-{random_string}" | ||
network = docker_client.networks.create(network_name) | ||
yield network_name | ||
network.remove() | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def mysql_user_password(random_string): | ||
"""A random password for authenticating to the mysql service""" | ||
return f"user-{random_string}" | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def esi_leap_port(): | ||
"""The esi-leap service will be published on this host port.""" | ||
return random.randint(10000, 30000) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def mysql_container(docker_client, test_network, mysql_user_password, random_string): | ||
"""Run a mysql container and wait until it is healthy. The fixture value | ||
is the container name.""" | ||
|
||
container_name = f"mysql-{random_string}" | ||
root_password = f"root-{random_string}" | ||
env = { | ||
"MYSQL_ROOT_PASSWORD": root_password, | ||
"MYSQL_DATABASE": "esi_leap", | ||
"MYSQL_USER": "esi_leap", | ||
"MYSQL_PASSWORD": mysql_user_password, | ||
} | ||
|
||
# We use the healthcheck so that we can wait until mysql is ready | ||
# before bringing up the esi-leap container. | ||
healthcheck = { | ||
"test": [ | ||
"CMD", | ||
"mysqladmin", | ||
"ping", | ||
f"-p{root_password}", | ||
], | ||
"start_period": int(30e9), | ||
"interval": int(5e9), | ||
} | ||
|
||
container = docker_client.containers.run( | ||
"docker.io/mysql:8", | ||
detach=True, | ||
network=test_network, | ||
name=container_name, | ||
environment=env, | ||
healthcheck=healthcheck, | ||
init=True, | ||
labels={"pytest": None, "esi-leap-test": random_string}, | ||
) | ||
|
||
for _ in range(30): | ||
container.reload() | ||
|
||
if container.health == "healthy": | ||
break | ||
|
||
time.sleep(1) | ||
else: | ||
raise OSError("failed to start mysql container") | ||
|
||
yield container_name | ||
|
||
container.remove(force=True) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def esi_leap_image(random_string): | ||
"""This will either build a new esi-leap image and return the name, or, if the | ||
ESI_LEAP_IMAGE environment variable is set, simply return the value of that | ||
variable.""" | ||
|
||
# Note that the := operator requires python >= 3.8 | ||
if image_name := os.getenv("ESI_LEAP_IMAGE"): | ||
return image_name | ||
|
||
image_name = f"esi-leap-{random_string}" | ||
subprocess.run( | ||
["docker", "build", "-t", image_name, "-f", "Containerfile", "."], check=True | ||
) | ||
return image_name | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def esi_leap_container( | ||
docker_client, | ||
test_network, | ||
mysql_container, | ||
mysql_user_password, | ||
tmp_path, | ||
random_string, | ||
esi_leap_port, | ||
esi_leap_image, | ||
): | ||
"""Run the esi-leap container. Create an esi-leap configuration file from | ||
the template and mount it at /etc/esi-leap/esi-leap.conf in the | ||
container. | ||
The service is exposed on esi_leap_port so that we can access it from our | ||
tests.""" | ||
|
||
container_name = f"esi-leap-api-{random_string}" | ||
config_file = tmp_path / "esi-leap.conf" | ||
with config_file.open("w") as fd: | ||
fd.write( | ||
esi_leap_config_template.format( | ||
**{ | ||
"tmp_path": tmp_path, | ||
"mysql_container": mysql_container, | ||
"mysql_user_password": mysql_user_password, | ||
} | ||
) | ||
) | ||
|
||
(tmp_path / "nodes").mkdir() | ||
(tmp_path / "locks").mkdir() | ||
|
||
container = docker_client.containers.run( | ||
esi_leap_image, | ||
detach=True, | ||
network=test_network, | ||
name=container_name, | ||
init=True, | ||
labels={"pytest": None, "esi-leap-test": random_string}, | ||
ports={"7777/tcp": esi_leap_port}, | ||
volumes=[f"{tmp_path}/esi-leap.conf:/etc/esi-leap/esi-leap.conf"], | ||
) | ||
|
||
for _ in range(30): | ||
try: | ||
res = requests.get(f"http://localhost:{esi_leap_port}/v1/offers") | ||
if res.status_code == 200: | ||
break | ||
except requests.RequestException: | ||
pass | ||
|
||
time.sleep(1) | ||
else: | ||
raise OSError("failed to start esi-leap container") | ||
|
||
yield container_name | ||
|
||
container.remove(force=True) | ||
|
||
|
||
def test_api_list_offers(esi_leap_container, esi_leap_port): | ||
res = requests.get(f"http://localhost:{esi_leap_port}/v1/offers") | ||
assert res.status_code == 200 | ||
assert res.json() == {"offers": []} | ||
|
||
|
||
def test_api_list_leases(esi_leap_container, esi_leap_port): | ||
res = requests.get(f"http://localhost:{esi_leap_port}/v1/leases") | ||
assert res.status_code == 200 | ||
assert res.json() == {"leases": []} | ||
|
||
|
||
def test_api_list_events(esi_leap_container, esi_leap_port): | ||
res = requests.get(f"http://localhost:{esi_leap_port}/v1/events") | ||
assert res.status_code == 200 | ||
assert res.json() == {"events": []} | ||
|
||
|
||
@pytest.mark.xfail(reason="nodes endpoint requires keystone") | ||
def test_api_list_nodes(esi_leap_container, esi_leap_port): | ||
res = requests.get(f"http://localhost:{esi_leap_port}/v1/nodes") | ||
assert res.status_code == 200 | ||
assert res.json() == {"nodes": []} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters