Skip to content

Commit 3695143

Browse files
committed
get wiremock runner working
1 parent 6a47f01 commit 3695143

File tree

6 files changed

+255
-34
lines changed

6 files changed

+255
-34
lines changed

wiremock/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ build
66
.terraform*
77
terraform.tfstate*
88
*.zip
9+
.wiremock

wiremock/README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ WireMock on LocalStack
33

44
This repo contains a [LocalStack Extension](https://github.com/localstack/localstack-extensions) that facilitates developing [WireMock](https://wiremock.org)-based applications locally.
55

6+
The extension supports two modes:
7+
- **OSS WireMock**: Uses the open-source `wiremock/wiremock` image (default)
8+
- **WireMock Runner**: Uses `wiremock/wiremock-runner` with WireMock Cloud integration (requires API token)
9+
610
## Prerequisites
711

812
* Docker
913
* LocalStack Pro (free trial available)
1014
* `localstack` CLI
1115
* `make`
16+
* [WireMock CLI](https://docs.wiremock.io/cli/overview) (for WireMock Runner mode)
1217

1318
## Install from GitHub repository
1419

1520
This extension can be installed directly from this Github repo via:
1621

1722
```bash
18-
localstack extensions install "git+https://github.com/whummer/localstack-utils.git#egg=localstack-wiremock&subdirectory=localstack-wiremock"
23+
localstack extensions install "git+https://github.com/localstack/localstack-extensions.git#egg=localstack-wiremock&subdirectory=wiremock"
1924
```
2025

2126
## Install local development version
@@ -39,3 +44,74 @@ You can then start LocalStack with `EXTENSION_DEV_MODE=1` to load all enabled ex
3944
```bash
4045
EXTENSION_DEV_MODE=1 localstack start
4146
```
47+
48+
## Usage
49+
50+
### OSS WireMock Mode (Default)
51+
52+
Start LocalStack without any special configuration:
53+
54+
```bash
55+
localstack start
56+
```
57+
58+
The WireMock server will be available at `http://wiremock.localhost.localstack.cloud:4566`.
59+
60+
You can import stubs using the WireMock Admin API:
61+
62+
```bash
63+
curl -X POST -H "Content-Type: application/json" \
64+
--data-binary "@stubs.json" \
65+
"http://wiremock.localhost.localstack.cloud:4566/__admin/mappings/import"
66+
```
67+
68+
### WireMock Runner Mode (Cloud Integration)
69+
70+
To use WireMock Runner with WireMock Cloud, you need:
71+
1. A WireMock Cloud API token
72+
2. A `.wiremock` directory with your mock API configuration
73+
74+
#### Step 1: Get your WireMock Cloud API Token
75+
76+
1. Sign up at [WireMock Cloud](https://app.wiremock.cloud)
77+
2. Go to Settings → API Tokens
78+
3. Create a new token
79+
80+
#### Step 2: Create your Mock API configuration
81+
82+
First, create a Mock API in WireMock Cloud, then pull the configuration locally:
83+
84+
```bash
85+
# Install WireMock CLI if not already installed
86+
npm install -g wiremock
87+
88+
# Login with your API token
89+
wiremock login
90+
91+
# Pull your Mock API configuration
92+
# Find your Mock API ID from the WireMock Cloud URL (e.g., https://app.wiremock.cloud/mock-apis/zwg1l/...)
93+
wiremock pull mock-api <mock-api-id>
94+
```
95+
96+
This creates a `.wiremock` directory with your `wiremock.yaml` configuration.
97+
98+
#### Step 3: Start LocalStack with WireMock Runner
99+
100+
```bash
101+
LOCALSTACK_WIREMOCK_API_TOKEN="your-api-token" \
102+
LOCALSTACK_WIREMOCK_CONFIG_DIR="/path/to/your/project" \
103+
localstack start
104+
```
105+
106+
**Environment Variables:**
107+
- `WIREMOCK_API_TOKEN`: Your WireMock Cloud API token (required for runner mode)
108+
- `WIREMOCK_CONFIG_DIR`: Path to the directory containing your `.wiremock` folder (required for runner mode)
109+
110+
Note: When using the LocalStack CLI, prefix environment variables with `LOCALSTACK_` to forward them to the container.
111+
112+
## Sample Application
113+
114+
See the `sample-app/` directory for a complete example using Terraform that demonstrates:
115+
- Creating an API Gateway
116+
- Lambda function that calls WireMock stubs
117+
- Integration testing with mocked external APIs

wiremock/bin/create-stubs.sh

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
#!/bin/bash
22

3-
echo "Downloading WireMock stub definitions..."
3+
# Import stubs into OSS WireMock (for WireMock Runner, use setup-wiremock-runner.sh)
44

5-
# Define the URL for the stub definitions and the temporary file path
6-
STUBS_URL="https://library.wiremock.org/catalog/api/p/personio.de/personio-de-personnel/personio.de-personnel-stubs.json"
5+
STUBS_URL="${STUBS_URL:-https://library.wiremock.org/catalog/api/p/personio.de/personio-de-personnel/personio.de-personnel-stubs.json}"
76
TMP_STUBS_FILE="/tmp/personio-stubs.json"
7+
WIREMOCK_URL="${WIREMOCK_URL:-http://wiremock.localhost.localstack.cloud:4566}"
88

9-
# Define the WireMock server URL
10-
WIREMOCK_URL="http://localhost:8080"
11-
12-
# Download the stub definitions
9+
echo "Downloading stubs from ${STUBS_URL}..."
1310
curl -s -o "$TMP_STUBS_FILE" "$STUBS_URL"
1411

15-
echo "Download complete. Stubs saved to $TMP_STUBS_FILE"
16-
echo "Importing stubs into WireMock..."
17-
18-
# Send a POST request to WireMock's import endpoint with the downloaded file
19-
curl -v -X POST -H "Content-Type: application/json" --data-binary "@$TMP_STUBS_FILE" "$WIREMOCK_URL/__admin/mappings/import"
12+
echo "Importing stubs into WireMock at ${WIREMOCK_URL}..."
13+
curl -v -X POST -H "Content-Type: application/json" --data-binary "@$TMP_STUBS_FILE" "${WIREMOCK_URL}/__admin/mappings/import"
2014

2115
echo ""
22-
echo "WireMock stub import request sent."
16+
echo "Verify stubs at: ${WIREMOCK_URL}/__admin/mappings"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/bin/bash
2+
3+
# Setup WireMock Runner for LocalStack
4+
5+
set -e
6+
7+
MOCK_API_NAME="${MOCK_API_NAME:-wiremock}"
8+
MOCK_API_PORT="${MOCK_API_PORT:-8080}"
9+
WIREMOCK_DIR="${WIREMOCK_DIR:-.wiremock}"
10+
STUBS_URL="${STUBS_URL:-https://library.wiremock.org/catalog/api/p/personio.de/personio-de-personnel/personio.de-personnel-stubs.json}"
11+
12+
echo "=== WireMock Runner Setup ==="
13+
14+
# Check prerequisites
15+
if ! command -v wiremock &> /dev/null; then
16+
echo "Error: WireMock CLI not installed. Run: npm install -g wiremock"
17+
exit 1
18+
fi
19+
20+
if ! wiremock mock-apis list &> /dev/null; then
21+
echo "Error: Not logged in. Run: wiremock login"
22+
exit 1
23+
fi
24+
25+
echo "✓ CLI authenticated"
26+
27+
# Create Mock API
28+
echo "Creating Mock API '${MOCK_API_NAME}'..."
29+
wiremock mock-apis create "${MOCK_API_NAME}" 2>&1 || echo "Note: May already exist"
30+
31+
wiremock mock-apis list
32+
echo ""
33+
echo "Enter Mock API ID:"
34+
read -r MOCK_API_ID
35+
36+
[ -z "$MOCK_API_ID" ] && { echo "Error: Mock API ID required"; exit 1; }
37+
38+
# Create config
39+
mkdir -p "${WIREMOCK_DIR}/stubs/${MOCK_API_NAME}/mappings"
40+
41+
cat > "${WIREMOCK_DIR}/wiremock.yaml" << EOF
42+
services:
43+
${MOCK_API_NAME}:
44+
type: 'REST'
45+
name: '${MOCK_API_NAME}'
46+
port: ${MOCK_API_PORT}
47+
path: '/'
48+
cloud_id: '${MOCK_API_ID}'
49+
EOF
50+
51+
echo "✓ Created ${WIREMOCK_DIR}/wiremock.yaml"
52+
53+
# Download stubs
54+
TMP_STUBS_FILE="/tmp/wiremock-stubs.json"
55+
curl -s -o "$TMP_STUBS_FILE" "$STUBS_URL"
56+
57+
if [ -f "$TMP_STUBS_FILE" ] && command -v jq &> /dev/null; then
58+
MAPPING_COUNT=$(jq '.mappings | length' "$TMP_STUBS_FILE" 2>/dev/null || jq 'length' "$TMP_STUBS_FILE" 2>/dev/null || echo "0")
59+
if [ "$MAPPING_COUNT" != "0" ] && [ "$MAPPING_COUNT" != "null" ]; then
60+
for i in $(seq 0 $((MAPPING_COUNT - 1))); do
61+
jq ".mappings[$i] // .[$i]" "$TMP_STUBS_FILE" > "${WIREMOCK_DIR}/stubs/${MOCK_API_NAME}/mappings/mapping-$i.json" 2>/dev/null
62+
done
63+
echo "✓ Extracted ${MAPPING_COUNT} stubs"
64+
fi
65+
fi
66+
67+
echo ""
68+
echo "=== Setup Complete ==="
69+
echo ""
70+
echo "Start LocalStack with:"
71+
echo " LOCALSTACK_WIREMOCK_API_TOKEN=\"your-token\" \\"
72+
echo " LOCALSTACK_WIREMOCK_CONFIG_DIR=\"$(pwd)\" \\"
73+
echo " localstack start"
74+
echo ""
75+
echo "WireMock available at: http://wiremock.localhost.localstack.cloud:4566"
Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,90 @@
1+
import logging
12
import os
3+
from pathlib import Path
24

3-
from localstack.utils.container_utils.container_client import Util
5+
from localstack import config, constants
46
from localstack_wiremock.utils.docker import ProxiedDockerContainerExtension
57

68

7-
# Environment variable for WireMock Cloud API token - note: if this value is specified, then the
8-
# `wiremock/wiremock-runner` image is being used, otherwise the `wiremock/wiremock` OSS image.
9+
LOG = logging.getLogger(__name__)
10+
11+
# If set, uses wiremock-runner image; otherwise uses OSS wiremock image
912
ENV_WIREMOCK_API_TOKEN = "WIREMOCK_API_TOKEN"
10-
# container port for WireMock endpoint - TODO make configurable over time
11-
PORT = 8080
13+
# Host path to directory containing .wiremock/ (required for runner mode)
14+
ENV_WIREMOCK_CONFIG_DIR = "WIREMOCK_CONFIG_DIR"
15+
16+
SERVICE_PORT = 8080 # Mock API port
17+
ADMIN_PORT = 9999 # Admin interface port (runner mode)
1218

1319

1420
class WireMockExtension(ProxiedDockerContainerExtension):
1521
name = "localstack-wiremock"
1622

1723
HOST = "wiremock.<domain>"
18-
# name of the OSS Docker image
1924
DOCKER_IMAGE = "wiremock/wiremock"
20-
# name of the WireMock Cloud runner Docker image
2125
DOCKER_IMAGE_RUNNER = "wiremock/wiremock-runner"
22-
# name of the container
2326
CONTAINER_NAME = "ls-wiremock"
2427

2528
def __init__(self):
2629
env_vars = {}
2730
image_name = self.DOCKER_IMAGE
2831
volumes = None
32+
container_ports = [SERVICE_PORT]
33+
health_check_path = "/__admin/health"
34+
health_check_retries = 40
35+
health_check_sleep = 1
36+
2937
if api_token := os.getenv(ENV_WIREMOCK_API_TOKEN):
30-
env_vars["WMC_ADMIN_PORT"] = str(PORT)
38+
# WireMock Runner mode
39+
env_vars["WMC_ADMIN_PORT"] = str(ADMIN_PORT)
3140
env_vars["WMC_API_TOKEN"] = api_token
3241
env_vars["WMC_RUNNER_ENABLED"] = "true"
3342
image_name = self.DOCKER_IMAGE_RUNNER
34-
settings_file = Util.mountable_tmp_file()
35-
# TODO: set configs in YAML file
36-
volumes = [(settings_file, "/work/.wiremock/wiremock.yaml")]
43+
container_ports = [SERVICE_PORT, ADMIN_PORT]
44+
health_check_path = "/__/health"
45+
health_check_retries = 90
46+
health_check_sleep = 2
47+
48+
host_config_dir = os.getenv(ENV_WIREMOCK_CONFIG_DIR)
49+
50+
if not host_config_dir:
51+
LOG.error("WIREMOCK_CONFIG_DIR is required for WireMock runner mode")
52+
raise ValueError(
53+
"WIREMOCK_CONFIG_DIR must be set to the host path containing .wiremock/"
54+
)
55+
56+
host_wiremock_dir = os.path.join(host_config_dir, ".wiremock")
57+
58+
# Validate config in dev mode
59+
extension_dir = Path(__file__).parent.parent
60+
container_wiremock_dir = extension_dir / ".wiremock"
61+
container_wiremock_yaml = container_wiremock_dir / "wiremock.yaml"
62+
63+
if container_wiremock_dir.is_dir() and container_wiremock_yaml.is_file():
64+
LOG.info("WireMock config found at: %s", container_wiremock_dir)
65+
else:
66+
LOG.warning("Ensure %s/.wiremock/wiremock.yaml exists", host_config_dir)
67+
68+
LOG.info("Mounting WireMock config from: %s", host_wiremock_dir)
69+
volumes = [(host_wiremock_dir, "/work/.wiremock")]
70+
71+
health_check_port = ADMIN_PORT if api_token else SERVICE_PORT
72+
self._is_runner_mode = bool(api_token)
73+
3774
super().__init__(
3875
image_name=image_name,
39-
container_ports=[PORT],
76+
container_ports=container_ports,
4077
container_name=self.CONTAINER_NAME,
4178
host=self.HOST,
42-
volumes=volumes,
4379
env_vars=env_vars if env_vars else None,
80+
volumes=volumes,
81+
health_check_path=health_check_path,
82+
health_check_port=health_check_port,
83+
health_check_retries=health_check_retries,
84+
health_check_sleep=health_check_sleep,
4485
)
86+
87+
def on_platform_ready(self):
88+
url = f"http://wiremock.{constants.LOCALHOST_HOSTNAME}:{config.get_edge_port_http()}"
89+
mode = "Runner" if self._is_runner_mode else "OSS"
90+
LOG.info("WireMock %s extension ready: %s", mode, url)

wiremock/localstack_wiremock/utils/docker.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ class ProxiedDockerContainerExtension(Extension):
5353
env_vars: dict[str, str] | None = None
5454
"""Optional environment variables to pass to the container."""
5555

56+
health_check_path: str = "/__admin/health"
57+
"""Health check endpoint path to verify container is ready."""
58+
59+
health_check_port: int | None = None
60+
"""Port to use for health check. If None, uses the first container port."""
61+
62+
health_check_retries: int = 40
63+
"""Number of retries for health check."""
64+
65+
health_check_sleep: float = 1
66+
"""Sleep time between health check retries in seconds."""
67+
5668
def __init__(
5769
self,
5870
image_name: str,
@@ -65,6 +77,10 @@ def __init__(
6577
http2_ports: list[int] | None = None,
6678
volumes: list[SimpleVolumeBind] | None = None,
6779
env_vars: dict[str, str] | None = None,
80+
health_check_path: str = "/__admin/health",
81+
health_check_port: int | None = None,
82+
health_check_retries: int = 40,
83+
health_check_sleep: float = 1,
6884
):
6985
self.image_name = image_name
7086
self.container_ports = container_ports
@@ -76,6 +92,10 @@ def __init__(
7692
self.http2_ports = http2_ports
7793
self.volumes = volumes
7894
self.env_vars = env_vars
95+
self.health_check_path = health_check_path
96+
self.health_check_port = health_check_port
97+
self.health_check_retries = health_check_retries
98+
self.health_check_sleep = health_check_sleep
7999

80100
def update_gateway_routes(self, router: http.Router[http.RouteHandler]):
81101
if self.path:
@@ -128,20 +148,29 @@ def start_container(self) -> None:
128148
LOG.debug("Failed to start container %s: %s", container_name, e)
129149
raise
130150

131-
main_port = self.container_ports[0]
151+
health_port = self.health_check_port or self.container_ports[0]
132152
container_host = get_addressable_container_host()
153+
health_url = f"http://{container_host}:{health_port}{self.health_check_path}"
133154

134155
def _ping_endpoint():
135-
# TODO: allow defining a custom healthcheck endpoint ...
136-
response = requests.get(
137-
f"http://{container_host}:{main_port}/__admin/health"
138-
)
156+
LOG.debug("Health check: %s", health_url)
157+
response = requests.get(health_url, timeout=5)
139158
assert response.ok
140159

141160
try:
142-
retry(_ping_endpoint, retries=40, sleep=1)
161+
retry(
162+
_ping_endpoint,
163+
retries=self.health_check_retries,
164+
sleep=self.health_check_sleep,
165+
)
143166
except Exception as e:
144167
LOG.info("Failed to connect to container %s: %s", container_name, e)
168+
# Log container output for debugging
169+
try:
170+
logs = DOCKER_CLIENT.get_container_logs(container_name)
171+
LOG.info("Container logs for %s:\n%s", container_name, logs)
172+
except Exception:
173+
pass
145174
self._remove_container()
146175
raise
147176

0 commit comments

Comments
 (0)