feat: implement docs-site, legal docs, metrics standard, flux manifests

Closes leeworks-agents/api-company#5  (docs-site Astro scaffold)
Closes leeworks-agents/api-company#9  (metrics instrumentation standard)
Closes leeworks-agents/api-company#10 (Gitea Actions openapi aggregation pipeline)
Closes leeworks-agents/api-company#11 (docs-site Flux HelmRelease)
Closes leeworks-agents/api-company#12 (SEO blog posts x3)
Closes leeworks-agents/api-company#13 (legal docs ToS/Privacy/AUP)
Closes leeworks-agents/api-company#14 (DNS documentation)

## Changes

### docs/legal/
- terms-of-service.md — API usage, liability, account termination, governing law
- privacy-policy.md — request log retention (90d), no PII sold, data sharing
- acceptable-use-policy.md — rate limit abuse, scraping prohibition, resale ban

### docs/metrics-standard.md
- Defines api_requests_total, api_response_duration_seconds, api_data_freshness_seconds
- Fastify (TypeScript) and FastAPI (Python) reference middleware implementations
- Prometheus scrape config and Grafana dashboard guidance

### docs/registry.md
- Decision: use Gitea built-in container registry (no new infra)
- Image naming convention, auth, Kubernetes imagePullSecrets, ingress config

### docs/dns.md
- Required A records for all 6 subdomains
- cert-manager ClusterIssuer and Ingress TLS examples
- Verification commands and human-operator action items

### docs-site/
- Astro 4 + MDX + sitemap scaffold
- Base layout with nav linking all APIs, blog, RapidAPI, status
- Landing page with API cards
- Per-API Redoc viewer pages (zip-enrichment, holidays, air-quality)
- Blog index + 3 SEO blog posts (~1000 words each with JSON-LD)
- Dockerfile (multi-stage: node build + nginx serve)
- nginx.conf with gzip, caching, health endpoint

### flux/
- gitea-runner/: gitea-act-runner HelmRelease (org-scope, dind)
- monitoring/: kube-prometheus-stack + Gatus HelmReleases
  - Prometheus with pod annotation scraping
  - Grafana at grafana.leeworks.dev with persistence
  - Gatus status page at status.leeworks.dev, 90-day retention
- docs-site/: Deployment + Service + Ingress via raw chart
- api-company-source/: GitRepository + Kustomization reference manifests
- kustomization.yaml: root kustomize entry point (build validated)

### .gitea/workflows/build-docs.yaml
- Aggregates openapi.yaml from zip-enrichment, holidays, air-quality repos
- Builds Astro docs-site
- Pushes image to registry.leeworks.dev/leeworks-agents/docs-site
- Triggered on push to main, schedule daily 02:00 UTC, workflow_dispatch
This commit is contained in:
agent-company
2026-05-24 23:20:33 +00:00
parent 40631fee7c
commit a615b7ebfd
41 changed files with 2201 additions and 3 deletions
+144
View File
@@ -0,0 +1,144 @@
# DNS Configuration
**Last updated:** 2026-05-24
**Status:** Planned (Phase 6 pre-launch)
---
## DNS Provider
DNS for `leeworks.dev` is managed externally (by the human operator via their registrar/DNS provider). The agent cannot directly create DNS records. This document tracks the required records for human operator action.
---
## Required Records
All records should point to the cluster ingress IP. To find the current ingress IP:
```bash
kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
| Subdomain | Type | Target | Purpose | TLS Required |
|-----------|------|--------|---------|-------------|
| `zip.leeworks.dev` | A | `<cluster-ingress-ip>` | ZIP Enrichment API | Yes (cert-manager) |
| `holidays.leeworks.dev` | A | `<cluster-ingress-ip>` | Holidays API | Yes (cert-manager) |
| `aqi.leeworks.dev` | A | `<cluster-ingress-ip>` | Air Quality API | Yes (cert-manager) |
| `docs.leeworks.dev` | A | `<cluster-ingress-ip>` | Documentation site | Yes (cert-manager) |
| `status.leeworks.dev` | A | `<cluster-ingress-ip>` | Gatus status page | Yes (cert-manager) |
| `registry.leeworks.dev` | A | `<cluster-ingress-ip>` | Container registry (Gitea) | Yes (cert-manager) |
| `grafana.leeworks.dev` | A | `<cluster-ingress-ip>` | Grafana (internal/restricted) | Yes (cert-manager) |
---
## TLS Certificate Management
TLS certificates are issued automatically by **cert-manager** using Let's Encrypt (ACME HTTP-01 or DNS-01 challenge).
### Prerequisites
- cert-manager deployed in the cluster (part of Talos setup)
- A `ClusterIssuer` configured for Let's Encrypt
### ClusterIssuer (Let's Encrypt Production)
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: legal@leeworks.dev
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
```
### Example Ingress with TLS
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zip-enrichment-ingress
namespace: zip-enrichment
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- zip.leeworks.dev
secretName: zip-tls
rules:
- host: zip.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: zip-enrichment
port:
number: 3000
```
---
## Verification Steps
After DNS records are created:
```bash
# Check DNS resolution
dig zip.leeworks.dev +short
dig holidays.leeworks.dev +short
dig aqi.leeworks.dev +short
dig docs.leeworks.dev +short
dig status.leeworks.dev +short
dig registry.leeworks.dev +short
# Check TLS certificates (once services are deployed)
curl -v https://zip.leeworks.dev/health 2>&1 | grep -E "SSL|certificate|issuer"
# Check cert-manager issued certs
kubectl get certificates -A
# Expect HTTP 200 on health endpoints
for host in zip.leeworks.dev holidays.leeworks.dev aqi.leeworks.dev; do
echo -n "$host: "
curl -s -o /dev/null -w "%{http_code}" https://$host/health
echo
done
```
---
## Action Required (Human Operator)
The following actions require human operator access to the DNS provider:
1. Log into the DNS provider managing `leeworks.dev`
2. Find the cluster ingress IP: `kubectl get svc -n ingress-nginx ingress-nginx-controller`
3. Create/update the 6 A records listed in the table above
4. Verify propagation: `dig +trace zip.leeworks.dev`
DNS propagation typically takes 560 minutes.
---
## Current Status
- [ ] Cluster ingress IP confirmed
- [ ] `zip.leeworks.dev` → DNS record created
- [ ] `holidays.leeworks.dev` → DNS record created
- [ ] `aqi.leeworks.dev` → DNS record created
- [ ] `docs.leeworks.dev` → DNS record created
- [ ] `status.leeworks.dev` → DNS record created
- [ ] `registry.leeworks.dev` → DNS record created
- [ ] TLS certificates issued and valid for all 6 subdomains
+93
View File
@@ -0,0 +1,93 @@
# Acceptable Use Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Purpose
This Acceptable Use Policy ("AUP") defines the rules for using leeworks.dev APIs. It applies to all users regardless of plan. Violations may result in immediate account suspension.
## 2. Rate Limits and Abuse
### 2.1 Respect Your Plan Limits
Each subscription plan includes defined rate limits:
| Plan | Requests/min | Requests/month |
|------|-------------|---------------|
| Free | 10 | 500 |
| Basic | 60 | 10,000 |
| Pro | 300 | 100,000 |
| Ultra | 1,000 | 1,000,000 |
You must not exceed your plan's limits through any means.
### 2.2 Prohibited Rate Limit Circumvention
The following are explicitly prohibited:
- Using multiple API keys or accounts to aggregate quota
- Caching responses for redistribution beyond your own application
- Rotating IP addresses to avoid throttling
- Using proxies or VPNs specifically to bypass rate limits
## 3. Prohibited Uses
### 3.1 Data Scraping and Bulk Download
You may **not**:
- Download or cache the entire dataset backing any API
- Make sequential requests designed to reconstruct the underlying database
- Use automated tools to systematically extract all available data points
### 3.2 Resale and Redistribution
You may **not**:
- Resell, sublicense, or redistribute API access to third parties
- Build a competing API product that serves our data to others
- Offer a "proxy" service that wraps our API for other developers
### 3.3 Malicious and Illegal Use
You may **not**:
- Use the APIs for any illegal purpose under applicable law
- Use the APIs to harass, stalk, or harm any individual
- Attempt to compromise the security or integrity of our systems
- Reverse-engineer our APIs beyond what's documented in the OpenAPI spec
- Use the APIs to generate or distribute spam
### 3.4 Infrastructure Attacks
You may **not**:
- Perform denial-of-service attacks against our infrastructure
- Probe our systems for vulnerabilities without prior written authorization
- Exploit bugs or errors to gain elevated access
## 4. Acceptable Uses
The following are examples of acceptable use:
- Integrating ZIP code, holiday, or air quality data into your own product
- Building dashboards, mobile apps, or internal tools
- Academic research (within Free plan limits)
- Automated data fetching within your plan's rate limits
## 5. Monitoring and Enforcement
We continuously monitor API usage for abuse. Automated systems may flag suspicious patterns. Flagged accounts may be:
- Throttled further without notice
- Required to verify identity
- Temporarily suspended pending review
- Permanently terminated for serious violations
## 6. Reporting Abuse
If you observe misuse of our APIs (e.g., someone redistributing your API key), please report it to **legal@leeworks.dev** immediately.
## 7. Changes
We may update this AUP at any time. Significant changes will be announced with an updated effective date. Continued use constitutes acceptance.
## 8. Contact
Questions about this policy: **legal@leeworks.dev**
+96
View File
@@ -0,0 +1,96 @@
# Privacy Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Overview
leeworks.dev ("we", "us") operates the ZIP Enrichment, Holidays, and Air Quality APIs. This Privacy Policy describes what data we collect when you use our Services, how we use it, and your rights regarding that data.
## 2. What Data We Collect
### 2.1 Request Logs
When you make API calls, we log:
- API key identifier (hashed/truncated — not the full key)
- IP address of the requesting client
- HTTP method and endpoint path
- Response status code
- Request timestamp
- Response time (latency)
**We do not log the full content of request or response bodies unless required for debugging.**
### 2.2 Account Data (via RapidAPI)
If you subscribe through RapidAPI, your account data (name, email, billing information) is managed by RapidAPI, not by us. Please review [RapidAPI's Privacy Policy](https://rapidapi.com/privacy/).
### 2.3 Cookies and Tracking
The API endpoints themselves do not use cookies. Our documentation site (`docs.leeworks.dev`) may use minimal session cookies for navigation only — no analytics or tracking cookies.
## 3. How We Use Your Data
We use collected data to:
- Monitor API health and uptime
- Detect and prevent abuse (rate limit evasion, scraping)
- Debug issues and improve service reliability
- Generate aggregate usage statistics (anonymized)
- Respond to support requests
**We do not sell your personal data to third parties. Ever.**
## 4. Data Retention
| Data Type | Retention Period |
|-----------|-----------------|
| Request logs (IP + endpoint) | 90 days |
| Aggregated usage metrics | 12 months |
| Billing records (via RapidAPI) | Per RapidAPI policy |
After the retention period, logs are automatically deleted.
## 5. Data Sharing
We share data only in the following circumstances:
- **With RapidAPI**: billing and subscription management
- **Legal requirements**: if required by law, court order, or government request
- **Service providers**: hosting infrastructure providers (under data processing agreements)
We do not share raw request logs with any third parties.
## 6. Security
We take reasonable technical and organizational measures to protect your data:
- API keys are transmitted over HTTPS only
- Access to log storage is restricted to authorized personnel
- Our cluster uses Kubernetes RBAC and network policies
However, no system is 100% secure. If you discover a security vulnerability, please report it to legal@leeworks.dev.
## 7. Your Rights
Depending on your jurisdiction, you may have rights to:
- Access the personal data we hold about you
- Request deletion of your data
- Object to or restrict processing
To exercise these rights, contact us at legal@leeworks.dev. We will respond within 30 days.
## 8. Children's Privacy
Our Services are not directed at children under 13. We do not knowingly collect data from children. If you believe a child has submitted data, contact us and we will delete it promptly.
## 9. International Transfers
Our services are hosted in the United States. By using the Services, you consent to the transfer and processing of your data in the US.
## 10. Changes to This Policy
We may update this Privacy Policy periodically. We will notify users of material changes by updating the effective date above and posting a notice. Continued use of the Services after changes constitutes acceptance.
## 11. Contact
For privacy inquiries: **legal@leeworks.dev**
+93
View File
@@ -0,0 +1,93 @@
# Terms of Service
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Acceptance of Terms
By accessing or using any API offered by leeworks.dev ("Services"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.
## 2. Description of Services
leeworks.dev provides data API services including:
- ZIP Enrichment API (`zip.leeworks.dev`)
- Holidays API (`holidays.leeworks.dev`)
- Air Quality API (`aqi.leeworks.dev`)
These APIs are offered via RapidAPI and directly. Access requires a valid API key.
## 3. API Usage Limits
- Each plan has defined rate limits (requests per minute and per month). Exceeding your plan's limits will result in HTTP 429 responses.
- You must not circumvent rate limiting through multiple accounts, shared keys, or other technical means.
- Free and Basic plan users are limited to non-commercial use unless explicitly stated otherwise.
## 4. Prohibited Use
You may not use the Services to:
- Resell or redistribute the API data or API access without written permission
- Scrape, download, or replicate the underlying dataset in bulk
- Build a competing API product using our data
- Violate any applicable laws, including data privacy regulations
- Harass, harm, or interfere with other users or our infrastructure
See also the [Acceptable Use Policy](./acceptable-use-policy.md).
## 5. Account Registration and Security
- You are responsible for keeping your API key confidential.
- You are responsible for all activity under your API key.
- Notify us immediately at legal@leeworks.dev if you suspect unauthorized use.
## 6. Payment and Billing
- Paid plans are billed through RapidAPI according to their billing terms.
- Refunds are handled at our discretion on a case-by-case basis. Contact legal@leeworks.dev within 7 days of a charge.
- We reserve the right to change pricing with 30 days' notice.
## 7. Data Accuracy Disclaimer
The data provided by leeworks.dev APIs is sourced from public datasets. We make no warranty as to the accuracy, completeness, or fitness for any particular purpose. You use the data at your own risk.
## 8. Service Availability
- We target 99.9% uptime but make no formal SLA guarantee on free or Basic plans.
- We reserve the right to take the service down for maintenance with or without notice.
- See `status.leeworks.dev` for real-time uptime information.
## 9. Intellectual Property
- The APIs, documentation, and underlying software are the intellectual property of leeworks.dev.
- Response data may be used in your own products subject to these Terms.
- You may not claim ownership of the data or present it as proprietary to you.
## 10. Termination
We may suspend or terminate your access to the Services immediately, without prior notice, for:
- Violation of these Terms
- Suspected abuse or fraud
- Non-payment of applicable fees
Upon termination, your license to use the Services ceases immediately.
## 11. Limitation of Liability
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LEEWORKS.DEV SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS, ARISING OUT OF OR IN CONNECTION WITH YOUR USE OF THE SERVICES.
## 12. Indemnification
You agree to indemnify and hold harmless leeworks.dev from any claims, damages, or expenses (including legal fees) arising from your use of the Services or violation of these Terms.
## 13. Changes to Terms
We may modify these Terms at any time. We will post changes on this page with an updated effective date. Continued use of the Services after changes constitutes acceptance.
## 14. Governing Law
These Terms are governed by the laws of the United States. Any disputes shall be resolved in the courts of appropriate jurisdiction.
## 15. Contact
Questions about these Terms? Contact us at: **legal@leeworks.dev**
+309
View File
@@ -0,0 +1,309 @@
# API Metrics Instrumentation Standard
**Version:** 1.0
**Date:** 2026-05-24
**Applies to:** All leeworks.dev API services (zip-enrichment, holidays, air-quality)
---
## Overview
Every API service MUST expose Prometheus-compatible metrics at `GET /metrics`. This document defines the required metrics, label conventions, and provides reference middleware implementations for both Fastify (Node.js) and FastAPI (Python).
---
## Required Metrics
### 1. `api_requests_total`
| Field | Value |
|-------|-------|
| **Type** | Counter |
| **Description** | Total number of HTTP requests received |
| **Labels** | `api`, `route`, `method`, `status` |
**Label values:**
- `api`: one of `zip-enrichment`, `holidays`, `air-quality`
- `route`: the matched route pattern, e.g. `/v1/lookup`, `/v1/holidays`
- `method`: HTTP method, e.g. `GET`, `POST`
- `status`: HTTP status code as string, e.g. `200`, `404`, `429`, `403`
**Example:**
```
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="200"} 1234
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="429"} 12
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="403"} 3
```
---
### 2. `api_response_duration_seconds`
| Field | Value |
|-------|-------|
| **Type** | Histogram |
| **Description** | HTTP response latency in seconds |
| **Labels** | `api`, `route` |
| **Buckets** | `0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5` |
**Example:**
```
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.05"} 800
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.1"} 990
api_response_duration_seconds_sum{api="holidays",route="/v1/holidays"} 45.2
api_response_duration_seconds_count{api="holidays",route="/v1/holidays"} 1000
```
---
### 3. `api_data_freshness_seconds`
| Field | Value |
|-------|-------|
| **Type** | Gauge |
| **Description** | Seconds since the local dataset was last seeded/refreshed |
| **Labels** | `api`, `dataset` |
| **Unit** | Seconds (Unix timestamp diff: `now - last_seed_time`) |
**Label values:**
- `dataset`: a descriptive name for the dataset, e.g. `zip_codes`, `us_holidays`, `aqi_readings`
**Example:**
```
api_data_freshness_seconds{api="air-quality",dataset="aqi_readings"} 86400
api_data_freshness_seconds{api="zip-enrichment",dataset="zip_codes"} 2592000
```
A value of `0` means freshly seeded; values growing toward `2592000` (30 days) are expected for monthly re-seed schedules.
---
## Reference Implementations
### Fastify (Node.js/TypeScript)
Install dependencies:
```bash
npm install prom-client
```
**`src/metrics.ts`:**
```typescript
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
export const register = new Registry();
export const requestsTotal = new Counter({
name: 'api_requests_total',
help: 'Total number of HTTP requests received',
labelNames: ['api', 'route', 'method', 'status'],
registers: [register],
});
export const responseDuration = new Histogram({
name: 'api_response_duration_seconds',
help: 'HTTP response latency in seconds',
labelNames: ['api', 'route'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
registers: [register],
});
export const dataFreshness = new Gauge({
name: 'api_data_freshness_seconds',
help: 'Seconds since the local dataset was last seeded',
labelNames: ['api', 'dataset'],
registers: [register],
});
```
**`src/metricsMiddleware.ts`:**
```typescript
import { FastifyPluginAsync } from 'fastify';
import { register, requestsTotal, responseDuration } from './metrics';
const API_NAME = process.env.API_NAME ?? 'unknown'; // set per service
export const metricsPlugin: FastifyPluginAsync = async (fastify) => {
// Expose /metrics endpoint
fastify.get('/metrics', async (_req, reply) => {
reply.header('Content-Type', register.contentType);
return register.metrics();
});
// Instrument all routes
fastify.addHook('onRequest', async (request, _reply) => {
(request as any)._startTime = process.hrtime.bigint();
});
fastify.addHook('onResponse', async (request, reply) => {
const startTime = (request as any)._startTime as bigint;
const durationMs = Number(process.hrtime.bigint() - startTime) / 1e6;
const route = request.routerPath ?? request.url;
requestsTotal.labels(API_NAME, route, request.method, String(reply.statusCode)).inc();
responseDuration.labels(API_NAME, route).observe(durationMs / 1000);
});
};
```
**Register in main:**
```typescript
import { metricsPlugin } from './metricsMiddleware';
await fastify.register(metricsPlugin);
```
**Update data freshness gauge (call after each seed):**
```typescript
import { dataFreshness } from './metrics';
// Call this after each DB seed completes:
dataFreshness.labels('zip-enrichment', 'zip_codes').set(0);
// Or set it to seconds since last seed on startup:
dataFreshness.labels('zip-enrichment', 'zip_codes').set(secondsSinceLastSeed);
```
---
### FastAPI (Python)
Install dependencies:
```bash
pip install prometheus-client starlette
```
**`metrics.py`:**
```python
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, CollectorRegistry
registry = CollectorRegistry()
requests_total = Counter(
'api_requests_total',
'Total number of HTTP requests received',
['api', 'route', 'method', 'status'],
registry=registry,
)
response_duration = Histogram(
'api_response_duration_seconds',
'HTTP response latency in seconds',
['api', 'route'],
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
registry=registry,
)
data_freshness = Gauge(
'api_data_freshness_seconds',
'Seconds since the local dataset was last seeded',
['api', 'dataset'],
registry=registry,
)
```
**`metrics_middleware.py`:**
```python
import time
import os
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from metrics import requests_total, response_duration, registry
API_NAME = os.getenv("API_NAME", "unknown")
class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
route = request.url.path
requests_total.labels(
api=API_NAME,
route=route,
method=request.method,
status=str(response.status_code),
).inc()
response_duration.labels(api=API_NAME, route=route).observe(duration)
return response
async def metrics_endpoint(request: Request):
return Response(
generate_latest(registry),
media_type=CONTENT_TYPE_LATEST,
)
```
**Register in FastAPI app:**
```python
from fastapi import FastAPI
from starlette.routing import Route
from metrics_middleware import MetricsMiddleware, metrics_endpoint
app = FastAPI()
app.add_middleware(MetricsMiddleware)
app.add_route("/metrics", metrics_endpoint)
```
---
## Prometheus Scrape Configuration
Add to your Prometheus `scrape_configs` (or ServiceMonitor for kube-prometheus-stack):
```yaml
# prometheus-additional-scrapes.yaml
- job_name: 'leeworks-apis'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
```
Add annotations to each API pod:
```yaml
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000" # or 8000 for FastAPI
prometheus.io/path: "/metrics"
```
---
## Grafana Dashboard
A reference dashboard JSON is available at `docs/grafana-api-dashboard.json` (TBD — will be committed once Grafana is deployed per issue #7).
Key panels to include:
1. Request rate by API and status (`rate(api_requests_total[5m])`)
2. P50/P95/P99 latency (`histogram_quantile(0.99, rate(api_response_duration_seconds_bucket[5m]))`)
3. Error rate = non-2xx / total requests
4. Data freshness gauge per API
5. Request volume heatmap
---
## Compliance Checklist
Before marking an API server PR as ready:
- [ ] `GET /metrics` returns `text/plain; version=0.0.4; charset=utf-8`
- [ ] `api_requests_total` increments on every request with correct labels
- [ ] `api_response_duration_seconds` has observations on every request
- [ ] `api_data_freshness_seconds` is set on startup and after each seed
- [ ] Pod annotations for Prometheus scraping are present in the Helm chart values
- [ ] `API_NAME` env var is set correctly per deployment
+163
View File
@@ -0,0 +1,163 @@
# Container Registry: registry.leeworks.dev
**Decision Date:** 2026-05-24
**Status:** Planned (Phase 0 prerequisite)
---
## Decision: Use Gitea's Built-in Container Registry
We will use **Gitea's built-in container registry** (OCI-compatible, enabled via `GITEA_CONTAINER_REGISTRY`) rather than deploying a separate `distribution/distribution` instance.
### Rationale
1. **No new infra** — Gitea is already deployed; enabling the container registry is a config flag, not a new deployment.
2. **Integrated auth** — API keys, org-scoped tokens, and CI secrets work natively with the Gitea registry.
3. **Simpler CI** — Gitea Actions workflows can use `${{ secrets.GITEA_TOKEN }}` to push to `gitea.leeworks.dev/leeworks-agents/<image>`.
4. **OCI compliance** — Gitea's container registry is OCI v1 compliant, compatible with Docker, Podman, and Kubernetes image pulls.
---
## Registry Hostname
```
registry.leeworks.dev
```
This will be a reverse proxy/ingress alias for `gitea.leeworks.dev` (Gitea's container registry endpoint).
Alternatively, Docker clients can use the Gitea hostname directly:
```
gitea.leeworks.dev/leeworks-agents/<image>:<tag>
```
If a separate hostname is preferred by the operator, configure an Nginx ingress to proxy `registry.leeworks.dev` → Gitea's container registry port.
---
## Image Naming Convention
```
registry.leeworks.dev/leeworks-agents/<repo-name>:<tag>
```
| API | Image |
|-----|-------|
| ZIP Enrichment | `registry.leeworks.dev/leeworks-agents/zip-enrichment:latest` |
| Holidays | `registry.leeworks.dev/leeworks-agents/holidays:latest` |
| Air Quality | `registry.leeworks.dev/leeworks-agents/air-quality:latest` |
| Docs Site | `registry.leeworks.dev/leeworks-agents/docs-site:latest` |
Tags should also include the git SHA for traceability: `:<sha>` in addition to `:latest`.
---
## Authentication
### Pushing from CI (Gitea Actions)
```yaml
- name: Log in to registry
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login registry.leeworks.dev \
-u ${{ gitea.actor }} --password-stdin
- name: Build and push
run: |
docker build -t registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} .
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }}
docker tag registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} \
registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
```
### Pulling from Kubernetes
Create an image pull secret in each namespace:
```bash
kubectl create secret docker-registry gitea-registry \
--docker-server=registry.leeworks.dev \
--docker-username=<gitea-user> \
--docker-password=<gitea-token> \
--docker-email=ci@leeworks.dev \
-n <namespace>
```
Reference in pod spec:
```yaml
spec:
imagePullSecrets:
- name: gitea-registry
```
---
## Enabling Gitea Container Registry
If not already enabled, the Gitea administrator needs to ensure:
1. In `app.ini` (or Helm values), container registry is enabled:
```ini
[packages]
ENABLED = true
```
2. The Gitea service is accessible on port 443 at `gitea.leeworks.dev`.
3. If using `registry.leeworks.dev` as an alias, configure an Nginx Ingress:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry-ingress
namespace: gitea
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
tls:
- hosts:
- registry.leeworks.dev
secretName: registry-tls
rules:
- host: registry.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
```
---
## Verification
```bash
# Test login
docker login registry.leeworks.dev -u <user> -p <token>
# Test push
docker pull alpine:latest
docker tag alpine:latest registry.leeworks.dev/leeworks-agents/test:latest
docker push registry.leeworks.dev/leeworks-agents/test:latest
# Test pull from cluster
kubectl run test-pull --image=registry.leeworks.dev/leeworks-agents/test:latest \
--image-pull-policy=Always --rm -it --restart=Never -- echo "Registry works"
```
---
## Phase 4 Reference
All API repos should update their `ROADMAP.md §Phase 4` to reference:
```
registry.leeworks.dev/leeworks-agents/<repo>:<tag>
```
as the image target for CI pushes and Flux HelmRelease image references.