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
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.mypy_cache
.pytest_cache
.pycache
.venv
__pycache__
*.pyc
*.egg-info
.coverage
coverage-data
data
act
act_*.tar.gz

1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PYTEST_COV=--cov=acme_api --cov=tests
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches:
- main
- master
pull_request:
workflow_dispatch:

jobs:
quality:
name: Quality Gates
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt

- name: Install dependencies
run: make dev

- name: Run local release gates
run: make combined-check
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

*.egg-info/
.venv/
.pycache/
.mypy_cache/
__pycache__
*.pyc
.pytest_cache/

coverage-data/
.coverage

data/

/act
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Agent Notes

## Project Overview

**acme.api** is a lightweight, self-hosted REST service for managing ACME certificates via DNS-01 validation. It delegates the ACME protocol to `acme.sh` and exposes a modern HTTP API for certificate lifecycle management, automatic renewals, and webhook notifications. See `docs/outline.md` for full design.

## Architecture (v1)

- **Backend**: `acme.sh` (only supported ACME implementation at this time).
- **Validation**: DNS-01 only; DNS providers are admin-defined aliases referenced by name in the API.
- **Deployment**: Single Docker container; certificates written atomically to a shared filesystem (`/certificates/<domain>/`).
- **Config**: `config.yaml` (YAML-based, parsed via PyYAML).

## Directory Layout

```
acme_api/ # main package: API, auth, DB, backend wrapper, scheduler, webhooks
tests/ # pytest test suite; integration tests and fixtures live here
dev/ # helper scripts (e.g. per-file coverage checker)
docs/ # project outline and design docs
Makefile # all build/lint/test commands
```

## Development Standards

- Follow PEP 8 and modern Python practices.
- Use type hints for all functions and methods.
- Write maintainable, modular, testable, and well-documented code.
- Add docstrings for all public functions and classes.
- Configuration is stored in `config.yaml`.

## Project Commands

| Command | Description |
|---|---|
| `make dev` | Set up venv + install all dependencies (required before other targets). |
| `make deps-update` | Regenerate pinned `requirements.txt` / `requirements-dev.txt` from pyproject.toml via pip-tools. |
| `make test [TEST=...]` | Run pytest with coverage. Scope to a file/path with `TEST=path/to/test.py`. Runs per-file coverage gate (default 80%). |
| `make typecheck` | Run mypy (strict mode, Python 3.14). |
| `make lint` | Run flake8 + pylint (fail-under 10.0). |
| `make isort` | Check import ordering (--check-only --diff). |
| `make format` | Apply isort formatting in-place. |
| `make check-forbidden-imports` | Reject imports of intentionally unsupported packages. |
| `make combined-check` | Run typecheck, lint, flake8, isort, check-max-lines, and test in one shot. |
| `make simulate-ci` | Run the GitHub Actions workflow locally with `act` (requires ACT_* env settings). |
| `make build` | Build Docker image via docker compose. |
| `make start` | Start the service (depends on build). |
| `make stop` | Stop the service. |
| `make logs` | Follow container logs. |

## Fixtures And Documentation

- Tests live in `tests/`; end-to-end mock-backed integration tests live in `tests/integration/`; fixtures live in `tests/fixtures/`.
- Use `make test` (not raw pytest) to ensure coverage gates are enforced.
- Update `README.md` with project description, installation instructions, usage, and other relevant information.
- Update this file (`AGENTS.md`) with useful information that may help future agents.
67 changes: 67 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
FROM python:3.14-slim AS builder

ENV PIP_NO_CACHE_DIR=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /build

COPY requirements.txt ./
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install --upgrade pip setuptools wheel \
&& /opt/venv/bin/pip install -r requirements.txt

COPY pyproject.toml README.md ./
COPY acme_api ./acme_api
RUN /opt/venv/bin/pip install --no-deps .

FROM python:3.14-slim AS runner

ARG ACME_SH_VERSION=3.1.3
ARG ACME_SH_SHA256=efd12b265252f8875269960b6b31830731ccce2b3e6ff8e7ecfbee21fde35ab4

ENV ACME_API_CONFIG=/config/config.yaml \
ACME_API_HOST=0.0.0.0 \
ACME_API_PORT=8080 \
ACME_SH_HOME=/acmesh \
ACME_SH_PATH=/usr/local/bin/acme.sh \
PATH="/opt/venv/bin:/home/acmeapi/.local/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl openssl socat \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --home-dir /home/acmeapi --shell /usr/sbin/nologin acmeapi \
&& mkdir -p /config /data /certificates /acmesh /opt/acme.sh \
&& chown -R acmeapi:acmeapi /config /data /certificates /acmesh /home/acmeapi \
&& tmp_dir="$(mktemp -d)" \
&& archive="$tmp_dir/acme.sh.tar.gz" \
&& curl -fsSL "https://github.com/acmesh-official/acme.sh/archive/refs/tags/${ACME_SH_VERSION}.tar.gz" -o "$archive" \
&& echo "${ACME_SH_SHA256} $archive" | sha256sum -c - \
&& tar -xzf "$archive" -C "$tmp_dir" --strip-components=1 \
&& cd "$tmp_dir" \
&& HOME=/home/acmeapi sh ./acme.sh --install --nocron --home /opt/acme.sh \
&& ln -sf /opt/acme.sh/acme.sh /usr/local/bin/acme.sh \
&& rm -rf "$tmp_dir" \
&& chown -R acmeapi:acmeapi /acmesh /home/acmeapi /opt/acme.sh

COPY --from=builder /opt/venv /opt/venv
COPY docker/entrypoint.sh /usr/local/bin/acme-api-entrypoint

RUN chmod +x /usr/local/bin/acme-api-entrypoint

USER acmeapi
WORKDIR /app

COPY --chown=acmeapi:acmeapi alembic.ini ./
COPY --chown=acmeapi:acmeapi alembic ./alembic

EXPOSE 8080
VOLUME ["/config", "/data", "/certificates", "/acmesh"]

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS "http://127.0.0.1:${ACME_API_PORT}/health" >/dev/null || exit 1

ENTRYPOINT ["acme-api-entrypoint"]
CMD ["acme-api"]
101 changes: 101 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
.PHONY: venv pip-cache-dir deps dev start build stop logs test typecheck lint flake8 format isort isort-fix check-max-lines check-forbidden-imports combined-check install-act simulate-ci deps-update
.ONESHELL:

export ROOT_PATH=$(abspath $(dir $(lastword $(MAKEFILE_LIST))))
export PYTHONPATH=$(ROOT_PATH)
COV_FILE_MIN ?= 80
PY_PATHS ?= acme_api tests
MYPY_PATHS ?= acme_api tests
MAX_FILE_LINES ?= 500
MAX_FILE_LINES_PATHS ?= acme_api tests

ACT_VERSION ?= 0.2.89
ACT_PLATFORM ?= Linux_x86_64
ACT_IMAGE ?= ghcr.io/catthehacker/ubuntu:full-24.04

ifneq (,$(wildcard $(ROOT_PATH)/.env))
include $(ROOT_PATH)/.env
export
endif

export PYTHONPYCACHEPREFIX=./.pycache

venv:
if [ ! -d ".venv" ]; then \
python3 -m venv .venv; \
fi

pip-cache-dir:
mkdir -p .venv/pip-cache

dev: venv pip-cache-dir
echo "Installing dependencies..."
.venv/bin/python3 -m pip install -q --cache-dir .venv/pip-cache --upgrade pip
.venv/bin/pip install -q --cache-dir .venv/pip-cache -r requirements-dev.txt
.venv/bin/pip install -q --cache-dir .venv/pip-cache -e .

deps-update: venv pip-cache-dir
.venv/bin/python3 -m pip install --cache-dir .venv/pip-cache --upgrade pip setuptools wheel pip-tools
.venv/bin/pip-compile pyproject.toml --output-file requirements.txt --strip-extras --pip-args "--cache-dir=.venv/pip-cache"
.venv/bin/pip-compile pyproject.toml --extra dev -c requirements.txt --output-file requirements-dev.txt --no-strip-extras --pip-args "--cache-dir=.venv/pip-cache"

deps: venv
.venv/bin/pip install -r requirements.txt

start: build
docker compose up -d

build:
docker compose build --pull

stop:
docker compose down

logs:
docker compose logs -f --tail=150

test: dev
set -e
.venv/bin/python3 scripts/check_forbidden_imports.py ${PY_PATHS}
.venv/bin/pytest -vvv --tb=short --color=yes ${PYTEST_COV} --cov-report=xml:coverage-data/coverage.xml --cov-report=html:coverage-data/htmlcov --cov-report=term --cov-report=json:coverage-data/coverage.json ${TEST}
ifeq ($(origin TEST), command line)
@echo "Skipping per-file coverage gate for scoped TEST=${TEST}"
else
.venv/bin/python3 dev/check_per_file_coverage.py --minimum "${COV_FILE_MIN}" --coverage-json coverage-data/coverage.json
endif

flake8: dev
.venv/bin/flake8 ${PY_PATHS} --max-line-length=120 --extend-ignore=E203 --count --show-source --statistics

lint: flake8
.venv/bin/pylint ${PY_PATHS}

format: dev
.venv/bin/isort ${PY_PATHS}

isort: dev
.venv/bin/isort ${PY_PATHS} --check-only --diff

check-max-lines: dev
.venv/bin/python3 scripts/check_max_lines.py --max-lines "${MAX_FILE_LINES}" ${MAX_FILE_LINES_PATHS}

check-forbidden-imports: dev
.venv/bin/python3 scripts/check_forbidden_imports.py ${PY_PATHS}

isort-fix: dev
.venv/bin/isort ${PY_PATHS} --interactive

typecheck: dev
.venv/bin/mypy ${MYPY_PATHS}

combined-check: typecheck lint flake8 isort check-max-lines test

install-act:
[ -x ./act ] && echo "act is already installed" && exit 0
wget https://github.com/nektos/act/releases/download/v$(ACT_VERSION)/act_$(ACT_PLATFORM).tar.gz
tar -xzf act_$(ACT_PLATFORM).tar.gz act
rm act_$(ACT_PLATFORM).tar.gz
chmod +x act

simulate-ci: install-act
./act -P ubuntu-latest=$(ACT_IMAGE)
Loading
Loading