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
3 changes: 3 additions & 0 deletions tests/antithesis/Dockerfile.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from scratch

COPY docker-compose.yml /docker-compose.yml
76 changes: 76 additions & 0 deletions tests/antithesis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
This directory enables integration of Antithesis with etcd. There are 4 containers running in this system: 3 that make up an etcd cluster (etcd0, etcd1, etcd2) and one that "[makes the system go](https://antithesis.com/docs/getting_started/basic_test_hookup/)" (client).

## Quickstart

### 1. Build and Tag the Docker Image

Run this command from the `antithesis/test-template` directory:

```bash
docker build . -f Dockerfile.client -t etcd-client:latest
```

### 2. (Optional) Check the Image Locally

You can verify your new image is built:

```bash
docker images | grep etcd-client
```

It should show something like:

```
etcd-client latest <IMAGE_ID> <DATE>
```

### 3. Use in Docker Compose

Run the following command from the root directory for Antithesis tests (`tests/antithesis`):

```bash
docker-compose up
```

The client will continuously check the health of the etcd nodes and print logs similar to:

```
[+] Running 4/4
✔ Container etcd0 Created 0.0s
✔ Container etcd2 Created 0.0s
✔ Container etcd1 Created 0.0s
✔ Container client Recreated 0.1s
Attaching to client, etcd0, etcd1, etcd2
etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.134294Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_ADVERTISE_CLIENT_URLS","variable-value":"http://etcd2.etcd:2379"}
etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.138501Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_ADVERTISE_PEER_URLS","variable-value":"http://etcd2:2380"}
etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.138646Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_CLUSTER","variable-value":"etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"}
etcd0 | {"level":"info","ts":"2025-04-14T07:23:25.138434Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_ADVERTISE_CLIENT_URLS","variable-value":"http://etcd0.etcd:2379"}
etcd0 | {"level":"info","ts":"2025-04-14T07:23:25.138582Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_ADVERTISE_PEER_URLS","variable-value":"http://etcd0:2380"}
etcd0 | {"level":"info","ts":"2025-04-14T07:23:25.138592Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_CLUSTER","variable-value":"etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"}

...
...
(skipping some repeated logs for brevity)
...
...

etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.484698Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
etcd1 | {"level":"info","ts":"2025-04-14T07:23:25.484092Z","caller":"embed/serve.go:210","msg":"serving client traffic insecurely; this is strongly discouraged!","traffic":"grpc+http","address":"[::]:2379"}
etcd0 | {"level":"info","ts":"2025-04-14T07:23:25.484563Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.485101Z","caller":"v3rpc/health.go:61","msg":"grpc service status changed","service":"","status":"SERVING"}
etcd1 | {"level":"info","ts":"2025-04-14T07:23:25.484130Z","caller":"etcdmain/main.go:44","msg":"notifying init daemon"}
etcd2 | {"level":"info","ts":"2025-04-14T07:23:25.485782Z","caller":"embed/serve.go:210","msg":"serving client traffic insecurely; this is strongly discouraged!","traffic":"grpc+http","address":"[::]:2379"}
etcd1 | {"level":"info","ts":"2025-04-14T07:23:25.484198Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
client | Client [entrypoint]: starting...
client | Client [entrypoint]: checking cluster health...
client | Client [entrypoint]: connection successful with etcd0
client | Client [entrypoint]: connection successful with etcd1
client | Client [entrypoint]: connection successful with etcd2
client | Client [entrypoint]: cluster is healthy!
```

And it will stay running indefinitely.

## Troubleshooting

- **Image Pull Errors**: If Docker can’t pull `etcd-client:latest`, make sure you built it locally (see the “Build and Tag” step) or push it to a registry that Compose can access.
49 changes: 49 additions & 0 deletions tests/antithesis/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
services:

etcd0:
image: 'gcr.io/etcd-development/etcd:v3.5.21'
container_name: etcd0
hostname: etcd0
environment:
ETCD_NAME: "etcd0"
ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd0:2380"
ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS: "http://etcd0.etcd:2379"
ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
ETCD_INITIAL_CLUSTER_STATE: "new"

etcd1:
image: 'gcr.io/etcd-development/etcd:v3.5.21'
container_name: etcd1
hostname: etcd1
environment:
ETCD_NAME: "etcd1"
ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd1:2380"
ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS: "http://etcd1.etcd:2379"
ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
ETCD_INITIAL_CLUSTER_STATE: "new"

etcd2:
image: 'gcr.io/etcd-development/etcd:v3.5.21'
container_name: etcd2
hostname: etcd2
environment:
ETCD_NAME: "etcd2"
ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd2:2380"
ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS: "http://etcd2.etcd:2379"
ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
ETCD_INITIAL_CLUSTER_STATE: "new"

client:
image: 'etcd-client:latest'
container_name: client
entrypoint: ["/entrypoint.py"]
14 changes: 14 additions & 0 deletions tests/antithesis/test-template/Dockerfile.client
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM ubuntu:latest

# Update package index first, then install Python
RUN apt-get update && \
apt-get install -y python3 python3-pip

# Then install additional Python packages
RUN apt-get install -y python3-etcd3 python3-numpy python3-protobuf python3-filelock

# Install Antithesis Python SDK
RUN pip install antithesis cffi --break-system-packages

# Copy your entrypoint script
COPY ./entrypoint/entrypoint.py /entrypoint.py
46 changes: 46 additions & 0 deletions tests/antithesis/test-template/entrypoint/entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env -S python3 -u

# This file serves as the client's entrypoint. It:
# 1. Confirms that all nodes in the cluster are available
# 2. Signals "setupComplete" using the Antithesis SDK

import etcd3, time

from antithesis.lifecycle import (
setup_complete,
)

SLEEP = 10

def check_health():

node_options = ["etcd0", "etcd1", "etcd2"]

for i in range(0, len(node_options)):
try:
c = etcd3.client(host=node_options[i], port=2379)
c.get('setting-up')
print(f"Client [entrypoint]: connection successful with {node_options[i]}")
except Exception as e:
print(f"Client [entrypoint]: connection failed with {node_options[i]}")
print(f"Client [entrypoint]: error: {e}")
return False
return True

print("Client [entrypoint]: starting...")

while True:
print("Client [entrypoint]: checking cluster health...")
if check_health():
print("Client [entrypoint]: cluster is healthy!")
break
else:
print(f"Client [entrypoint]: cluster is not healthy. retrying in {SLEEP} seconds...")
time.sleep(SLEEP)


# Here is the python format for setup_complete. At this point, our system is fully initialized and ready to test.
setup_complete({"Message":"ETCD cluster is healthy"})

# sleep infinity
time.sleep(31536000)