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
70 changes: 70 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
.env
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.pytest_cache/
htmlcov/
.nox/
coverage.xml
*.cover
.hypothesis/

# Poetry
dist/
build/
*.egg-info/

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Git
.git/
.gitignore

# Documentation
docs/
!README.md
mkdocs.yml

# Tests
tests/
conftest.py
test_*.py
*_test.py

# Development files
.pre-commit-config.yaml
docker/
artifacts/tmp/
.mypy_cache/

# Database
db.sqlite3
*.db

# Logs
*.log
33 changes: 33 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: E2E Tests

on:
pull_request:
permissions:
contents: read

jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: 3.13
cache: pip

- name: Install poetry
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1

- name: Install dependencies
run: poetry install --with tests

- name: Run tests
run: poetry run pytest -v -s e2e/
3 changes: 3 additions & 0 deletions e2e/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
66 changes: 66 additions & 0 deletions e2e/s3/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Multi-stage build for goosebit
FROM python:3.13-alpine AS builder

# Install build dependencies
RUN apk add --no-cache \
gcc \
musl-dev \
libffi-dev \
postgresql-dev \
curl

# Install Poetry
RUN pip install --no-cache-dir poetry

# Set working directory
WORKDIR /app

# Copy poetry files
COPY pyproject.toml poetry.lock ./

# Configure poetry: no dev dependencies, no virtual env (using container)
RUN poetry config virtualenvs.create false && poetry install --only=main --no-root

# Copy application code
COPY . .

# Install the application
RUN poetry install --only=main

# Production stage
FROM python:3.13-alpine

RUN apk add --no-cache curl

# Install runtime dependencies
RUN pip install --no-cache-dir gunicorn

# Create database directory
RUN mkdir -p db/

# Copy Python packages from builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# Set working directory
WORKDIR /app

# Copy application code
COPY . .

RUN mv /app/e2e/s3/aerich.toml /aerich.toml
RUN mv /app/e2e/s3/goosebit.yaml /app/goosebit.yaml

# Expose the application port
EXPOSE 60053

# Environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1

# We currently do not fully support multiple workers. For more information, see:
# https://github.com/UpstreamDataInc/goosebit/issues/125
ENV GUNICORN_CMD_ARGS="--workers 1 --enable-stdio-inheritance"

# Default command: run database migrations and start the server
CMD ["sh", "-c", "poetry run aerich upgrade && gunicorn --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:60053 goosebit:app"]
58 changes: 58 additions & 0 deletions e2e/s3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# gooseBit E2E tests

This directory contains a minimal end-to-end (E2E) test that boots the app with docker-compose and exercises initial setup, auth, and a couple of protected routes.

## Prerequisites

- Docker + Docker Compose
- Python 3.11+ (to run the test runner locally) or use `docker compose run` to run tests in a container

## Services

`docker-compose.yml` starts:

- minio (S3-compatible storage)
- goosebit (the FastAPI app, listening on port 60053)
- swupdate (test client container; not used by the smoke test yet)

## Run

1. Build and start the stack

```bash
cd e2e
docker compose up -d --build
```

2. Run the tests from the repo root (they target http://localhost:60053 by default):

```bash
poetry install --with tests
poetry run pytest -q e2e/tests
```

You can customize the base URL via env var:

```bash
E2E_BASE_URL=http://127.0.0.1:60053 poetry run pytest -q e2e/tests
```

3. Tear down when done:

```bash
docker compose down -v
```

## What the smoke test does

- Waits for `/docs` to become available
- POSTs to `/setup` to create the initial admin user
- Calls `/api/v1/devices` with Bearer token
- Sets the `session_id` cookie and loads `/ui/devices`
- Hits `/` and expects to land on the UI

## Next steps

- Add tests that upload artifacts to minio via the app and verify download URLs
- Exercise device flows (register device, assign rollout, check logs)
- Optionally run tests inside a container for hermetic CI
3 changes: 3 additions & 0 deletions e2e/s3/aerich.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tool.aerich]
tortoise_orm = "goosebit.db.config.TORTOISE_CONF"
location = "/app/goosebit/db/migrations"
62 changes: 62 additions & 0 deletions e2e/s3/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
services:
minio:
image: minio/minio:latest
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 3s
timeout: 5s
retries: 3
networks:
goosebit_network:
ipv4_address: 172.20.5.100

goosebit:
container_name: goosebit
build:
context: ../..
dockerfile: e2e/s3/Dockerfile
ports:
- "60053:60053"
environment:
- GOOSEBIT_DATABASE_URL=sqlite:///app/db/db.sqlite3
depends_on:
minio:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:60053"]
interval: 3s
timeout: 5s
retries: 3
networks:
goosebit_network:
ipv4_address: 172.20.5.102

swupdate:
container_name: swupdate
build:
context: ./swupdate
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
goosebit:
condition: service_healthy
networks:
goosebit_network:
ipv4_address: 172.20.5.103

networks:
goosebit_network:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.20.5.0/24
79 changes: 79 additions & 0 deletions e2e/s3/goosebit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Settings to adjust for each installation

# Port to host the server on, default:
#port: 60053 # GOOSE ;)

# Tenant for DDI endpoint
#tenant: DEFAULT

# Database to be used, default:
#db_uri: sqlite:///<project root>/db.sqlite3
#db_ssl_crt: /<project root>/ca-certificate.crt

# Path to the directory containing artifact files, default:
#artifacts_dir: /<project root>/artifacts

# Frequency that devices should check for available updates.
poll_time: 00:00:05

# Limit the number of updates that run at the same time. Can be used to avoid overloading the update server.
#max_concurrent_updates: 1000

# Whether to track the IP of the device when it polls. Useful for debugging, but can be turned off for privacy.
#track_device_ip: true

# Device authentication settings
# Token-based device authentication
#device_auth:
# enable: false

# The auth mode, one of:
# - strict: Strict mode, all devices must have keys, and all keys must be set via the API.
# - lax: Lax mode, devices which have keys must use them, but devices without keys do not require them.
# - setup: Setup mode, any devices polling with an auth token that don't have one will save it.
# Setup mode is designed for users migrating from other services, it makes setting up existing devices easy.
# mode: strict

# Secret key used for parsing user sessions. It is HIGHLY advised to pass this as an environment variable instead.
# Defaults to a randomized value. If this value is not set, user sessions will not persist when app restarts.
#secret_key: my_very_top_secret_key123

## Internal settings that usually don't need to be modified
metrics:
prometheus:
enable: false
logging:
version: 1
formatters:
simple:
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: simple
level: DEBUG
loggers:
tortoise:
handlers: [console]
level: WARNING
propagate: yes
aiosqlite:
handlers: [console]
level: WARNING
propagate: yes
multipart:
handlers: [console]
level: INFO
propagate: yes
root:
level: DEBUG
handlers: [console]
# Storage settings (default backend is filesystem)
storage:
backend: s3
s3:
bucket: goosebit
# region: us-east-1
endpoint_url: http://172.20.5.100:9000 # example for self-hosted min.io
access_key_id: minioadmin
secret_access_key: minioadmin
Loading
Loading