Skip to content

Commit 9cc43fb

Browse files
authored
Merge pull request #5 from siemens/docker-entrypoint
Add Docker entrypoint
2 parents 59f2c9f + f472847 commit 9cc43fb

File tree

7 files changed

+266
-12
lines changed

7 files changed

+266
-12
lines changed

.github/workflows/build_docker_images.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
# SPDX-FileCopyrightText: Copyright 2024 Siemens AG
1+
# SPDX-FileCopyrightText: Copyright 2025 Siemens AG
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

55
# This produces base image that contains the dependencies required for the test
66
# suite, including the build environment for liboqs; and a dev image, which is
7-
# used to run code quality checks in the CI pipeline
7+
# used to run code quality checks in the CI pipeline. It also builds the
8+
# production image, which is meant to be invoked by end-users who want to test
9+
# their CAs.
810

911
name: Build and push base docker images
1012

@@ -50,9 +52,12 @@ jobs:
5052
-f data/dockerfiles/Dockerfile.dev .
5153
5254
- name: Build and push the production test suite Docker image
55+
# This one is meant to be directly invoked by end-users who don't want to get into the details
56+
# of how the test suite works, they just want to run it to test their CA. For their convenience,
57+
# we give it a short name, to be invoked as `docker run --rm -it ghcr.io/siemens/cmp-test`
5358
run: |
5459
docker buildx build \
55-
--tag ghcr.io/${{ github.repository_owner }}/cmp-test-prod:latest \
60+
--tag ghcr.io/${{ github.repository_owner }}/cmp-test:latest \
5661
--build-arg BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/cmp-test-base:latest \
5762
--push \
5863
-f data/dockerfiles/Dockerfile.tests .

.github/workflows/check_quality.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ jobs:
4343
- name: Checkout code
4444
uses: actions/checkout@v4
4545
- name: RobotFramework style check
46-
run: robocop --report rules_by_error_type
46+
run: robocop check
47+
# We haven't settled on a style for RobotFramework yet, enforce check when consensus is reached
48+
continue-on-error: true
4749

4850
spelling_check:
4951
runs-on: ubuntu-22.04

data/dockerfiles/Dockerfile.tests

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ WORKDIR /app
1414

1515
COPY . /app
1616

17-
# TODO: Devise a procedure to make it easy to add user-specific tests that will
18-
# be executed along with the basic ones.
19-
CMD ["sh", "-c", "robot --pythonpath=./ --outputdir=reports/ scripts/smoke.robot"]
17+
# Run the entry point script, it takes care of command line argument parsing and
18+
# displaying documentation.
19+
ENTRYPOINT ["scripts/docker_entrypoint.py"]
20+
CMD ["--smoke"]

mock_ca/ca_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ def handle_issuing() -> bytes:
434434
data = request.get_data()
435435
pki_message, _rest = decoder.decode(data, asn1Spec=rfc9480.PKIMessage())
436436
pki_message = handler.process_normal_request(pki_message)
437-
logging.warning(f"Response: %s", pki_message.prettyPrint())
437+
logging.warning("Response: %s", pki_message.prettyPrint())
438438
response_data = encoder.encode(pki_message)
439439
return Response(response_data, content_type="application/octet-stream")
440440
except Exception as e:

readme.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,18 @@ the server and how the responses were processed.
2525

2626
The [contribution guidelines](CONTRIBUTING.md) explain how to contribute to the project.
2727

28+
# Quick start with Docker
29+
On a system where [Docker is available](https://docs.docker.com/engine/install/), the easiest way to run the test suite is `docker run --rm -it ghcr.io/siemens/cmp-test`. This will invoke a smoke test just to confirm that the basics are in place. Add `--help` to learn about what other commands are available.
2830

29-
# Configuration
31+
To run a minimal test against an actual CA, try `docker run --rm -it ghcr.io/siemens/cmp-test --minimal http://example.com --ephemeral` (replace the URL with your CMP endpoint).
32+
33+
A thorough evaluation that covers all the features of CMP requires a configuration file, where you specify preshared passwords, keys, algorithms to use, etc. (see `--customconfig` for details).
34+
35+
36+
# Advanced usage
37+
While the Docker-based approach makes it easy to get started, it essentially treats the test suite as a black box. However, if you want to customize, extend or debug it, it is necessary to dive deeper and understand how it works "under the hood".
38+
39+
## Configuration
3040
Create a Python virtual environment by installing the dependencies from `requirements.txt`:
3141

3242
1. Create a virtual environment: `python -m venv venv-cmp-tests`
@@ -38,12 +48,12 @@ Create a Python virtual environment by installing the dependencies from `require
3848

3949

4050

41-
# Usage
51+
## Usage
4252
1. Adjust the settings in the `config/local.robot` file to match your environment.
4353
2. Run `robot --variable environment:local tests` to run everything in `tests/` against the `local` environment.
4454
3. Explore `report.html` to see the results.
4555

46-
## Advanced usage examples
56+
## Additional RobotFramework commands
4757
You can run specific tests on specific environments by adjusting command line options. Consider this example:
4858
`robot --outputdir=out --variable environment:cloudpki --include crypto tests`
4959

scripts/docker_entrypoint.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/bin/python3
2+
3+
# SPDX-FileCopyrightText: Copyright 2025 Siemens AG
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
# pylint: disable=invalid-name
8+
9+
"""Entry point script for running the CMP test suite in a Docker container.
10+
11+
It aims to simplify the user experience, by exposing a minimal command line interface
12+
for doing simple things (e.g., run smoke tests), while also allowing complex use cases,
13+
when necessary (e.g., run all tests with a custom configuration).
14+
"""
15+
16+
import argparse
17+
import logging
18+
import os
19+
import subprocess
20+
import sys
21+
from pathlib import Path
22+
from urllib.parse import urlparse
23+
24+
log = logging.getLogger("cmptest")
25+
26+
27+
def valid_url(arg):
28+
"""Check whether the given URL is valid."""
29+
url = urlparse(arg)
30+
if all((url.scheme, url.netloc)):
31+
return arg
32+
raise argparse.ArgumentTypeError("Invalid URL")
33+
34+
35+
def run_robot_command(command, verbose=False):
36+
"""Run the robot command and return its exit code."""
37+
if verbose:
38+
log.info("Run: %s", command)
39+
try:
40+
with subprocess.Popen(
41+
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
42+
) as process:
43+
# Stream output in real-time
44+
while True:
45+
stdout_line = process.stdout.readline()
46+
stderr_line = process.stderr.readline()
47+
48+
if stdout_line:
49+
log.info(stdout_line.strip())
50+
if stderr_line:
51+
log.error(stderr_line.strip())
52+
53+
if not stdout_line and not stderr_line and process.poll() is not None:
54+
break
55+
56+
exit_code = process.poll()
57+
log.info("Command completed with exit code: %s", exit_code)
58+
return exit_code
59+
except OSError as e:
60+
log.error("Error executing command: %s", e)
61+
return 1
62+
63+
64+
class CustomConfigAction(argparse.Action):
65+
"""
66+
Custom argparse action to handle the --customconfig argument.
67+
68+
Distinguishes between explicit usage (with or without a value) and complete omission of the argument.
69+
"""
70+
71+
def __call__(self, parser, namespace, values, option_string=None): # noqa: D102 no docstring
72+
setattr(namespace, self.dest, values if values else Path("config/"))
73+
setattr(namespace, f"{self.dest}_explicit", True)
74+
75+
76+
def prepare_parser():
77+
"""Parse command line arguments."""
78+
parser = argparse.ArgumentParser(
79+
description="CMP test suite tool",
80+
formatter_class=argparse.RawDescriptionHelpFormatter,
81+
epilog="""Examples of usage, assume the docker image is called `image`:
82+
83+
1. Default behavior (no additional arguments):
84+
docker run --rm -it ghcr.io/siemens/cmp-test
85+
Executes: robot --pythonpath=./ --outputdir=/report --include smoke scripts/smoke.robot tests/
86+
Does not require any configuration, runs the smoke test and any other test with the `smoke` tag.
87+
88+
2. Passing a minimal URL:
89+
docker run --rm -it ghcr.io/siemens/cmp-test --minimal http://example.com
90+
Executes: robot --pythonpath=./ --outputdir=/report --include minimal --variable SERVER_URL:http://example.com tests/
91+
Runs only tests tagged `minimal`, which only require the server's address and nothing else.
92+
93+
3. Using a custom configuration:
94+
docker run --rm -it ghcr.io/siemens/cmp-test -v ./reports:/report -v ./config:/config image --customconfig
95+
Runs all tests with the custom configuration given in a directory mounted to config/, will save reports to /reports.
96+
97+
4. Passing arbitrary arguments to robot (note the `--`):
98+
docker run --rm -it ghcr.io/siemens/cmp-test --minimal http://example.com -- --dryrun
99+
Runs: robot --pythonpath=./ --outputdir=/report --include minimal --variable SERVER_URL:http://example.com tests/ \
100+
--dryrun
101+
""",
102+
)
103+
104+
group = parser.add_mutually_exclusive_group()
105+
group.add_argument("--smoke", action="store_true", help="Run smoke tests that don't require any configuration")
106+
group.add_argument("--minimal", help="Run tests that only need a CMP URL endpoint", type=valid_url, metavar="URL")
107+
group.add_argument(
108+
"--customconfig",
109+
help="Use custom configuration directory and run all tests",
110+
type=Path,
111+
nargs="?",
112+
default=Path("config/"),
113+
metavar="DIR",
114+
action=CustomConfigAction,
115+
)
116+
parser.add_argument("--tags", help="Run only tests with the given tags", type=str, nargs="+", default=[])
117+
parser.add_argument(
118+
"--ephemeral",
119+
help="Discard the detailed report generated by the test suite, the results printed on the screen are enough",
120+
action="store_true",
121+
default=False,
122+
)
123+
parser.add_argument(
124+
"--verbose", help="Display additional debugging information", action="store_true", default=False
125+
)
126+
127+
parser.add_argument(
128+
"robot_args", nargs=argparse.REMAINDER, help="Optional arguments to pass directlyto Robot Framework."
129+
)
130+
131+
# Add a flag to track explicit usage of --customconfig, to differentiate between
132+
# when it was provided with a value, without a value, or not at all
133+
parser.set_defaults(customconfig_explicit=False)
134+
135+
return parser
136+
137+
138+
def verify_report_directory(ephemeral, smoke, verbose):
139+
"""Check if the report directory exists and enforce some checks based on settings.
140+
141+
Args:
142+
ephemeral (bool): If True, we don't care about potentially losing the detailed report.
143+
smoke (bool): If True, we only run smoke tests.
144+
verbose (bool): If True, additional information will be logged.
145+
146+
"""
147+
if os.path.exists("/report"):
148+
if verbose:
149+
log.info("Report will be saved to /report (inside your container)")
150+
else:
151+
if ephemeral or smoke:
152+
log.info(
153+
"Running in ephemeral mode, only brief results on screen, no file report. Consider running the "
154+
"container with `-v /path/on/host:/report` to ensure the report is saved."
155+
)
156+
else:
157+
log.warning(
158+
"The /report directory does not exist, the detailed test report will be lost after "
159+
"the container is removed. Run with --ephemeral to ignore this and only consider the "
160+
"results shown on stdout, or run the container with `-v /path/on/host:/report` to "
161+
"ensure the report is saved."
162+
)
163+
sys.exit(1)
164+
165+
166+
def main():
167+
"""Run the CMP test suite."""
168+
parser = prepare_parser()
169+
args = parser.parse_args()
170+
171+
if args.verbose:
172+
log.debug(args)
173+
174+
verify_report_directory(args.ephemeral, args.smoke, args.verbose)
175+
176+
if args.robot_args:
177+
# One can pass additional arguments to RobotFramework by adding -- <robot args> at the end
178+
# of the command line, e.g. `docker run image --minimal http://example.com -- --dryrun`.
179+
# We prepare this piece here because we might need to add it later.
180+
additional_args = " " + " ".join(args.robot_args[1:]) # omit the "--" itself
181+
else:
182+
additional_args = ""
183+
184+
if args.smoke:
185+
# Run the smoke test in scripts/smoke.robot, as well as any other test with the `smoke` tag
186+
command = "robot --pythonpath=./ --outputdir=/report --include smoke scripts/smoke.robot"
187+
elif args.minimal:
188+
# A minimal batch of tests that only need to know the server's address and nothing else, it is the easiest
189+
# way to get a taste of what the test suite can do while still doing some actual work with a real server.
190+
command = f"robot --pythonpath=./ --outputdir=/report --include minimal --variable CA_CMP_URL:{args.minimal}"
191+
192+
elif args.customconfig_explicit:
193+
# The script was started with --customconfig, there are several possibilities:
194+
# [A] --customconfig without a specific path, so the default one is implied
195+
# [B] --customconfig with a specific path to a custom directory
196+
if args.customconfig == parser.get_default("customconfig"):
197+
# case [A], load the config from /config, where we expect it to be mounted when running within docker.
198+
config_path = Path("/config")
199+
else:
200+
# case [B], load the config from the path given by the user
201+
config_path = args.customconfig
202+
203+
if not os.path.exists(config_path / "custom.robot"):
204+
config_guide = """It must be mounted to /config in the docker container (override with --customconfig)
205+
and follow this structure:
206+
/config
207+
├── custom.robot - set the relevant variables, follow the examples given in config/ in the repo
208+
├── data/ - a directory with data files (keys, certificates, etc.) needed by your custom.robot"""
209+
log.warning("The configuration directory does not exist or is not structured correctly. %s", config_guide)
210+
sys.exit(1)
211+
212+
# If we end up here, it means that the user took the time to set up a configuration directory and adjust
213+
# it to the specifics of their CMP server. We'll run all tests and rely on the given configuration and
214+
# data (e.g., certificates, keypairs, preshared passwords, etc.)
215+
if args.tags:
216+
included_tags = "--include " + " --include ".join(args.tags)
217+
else:
218+
included_tags = ""
219+
220+
command = f"robot --pythonpath=./ --outputdir=/report --variable environment:custom {included_tags}"
221+
else:
222+
log.debug("No actionable command line arguments given, nothing to do")
223+
sys.exit(0)
224+
225+
# Prepare the final command, appending additional arguments, if any, and ensuring that test/ is in
226+
# the end.
227+
command = f"{command} {additional_args} tests/"
228+
229+
run_robot_command(command)
230+
231+
232+
if __name__ == "__main__":
233+
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)5s - %(message)s")
234+
235+
log.info("Starting CMP test suite")
236+
main()

tests/basic.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ CA Must Reject Malformed Request
2828
... a client-side error in the supplied input data. Ref: "3.3. General Form", "All applicable
2929
... "Client Error 4xx" or "Server Error 5xx" status codes MAY be used to inform the client
3030
... about errors."
31-
[Tags] negative rfc6712 robot:skip-on-failure status
31+
[Tags] negative rfc6712 robot:skip-on-failure status minimal
3232
${response}= Exchange Data With CA this dummy input is not a valid PKIMessage
3333
Should Be Equal
3434
... ${response.status_code}

0 commit comments

Comments
 (0)