Skip to content
Draft
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
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.11-slim

RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY miss_islington miss_islington

CMD ["python", "-m", "miss_islington"]
45 changes: 45 additions & 0 deletions infra/k8s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# miss-islington k8s infra

Kubernetes manifests for running miss-islington on cabotage instead of Heroku.

## What's here

`redis.yaml` and `cert.yaml` provision a standalone Redis instance in the `redis` namespace using the OpsTree Redis Operator (already running cluster-wide).
The cert is issued by the internal `operators-ca-issuer` ClusterIssuer, ECDSA P-256, 90-day rotation.

`ingress.yaml` goes in the `python` namespace where cabotage deploys the app. It's a standard nginx ingress with backend-protocol
HTTPS because cabotage serves on 8443/TLS behind a Service on port 443.

`generate-secrets.sh` creates the Redis password Secret and prints the full connection URI you need to set as `REDIS_URL` in cabotage.

## Setup

Generate the Redis password (once per cluster):

```
./infra/k8s/generate-secrets.sh
```

Apply the Redis CR and TLS cert:

```
kubectl apply -k infra/k8s -n redis
```

After cabotage has deployed the app and the Service exists, apply the ingress:

```
kubectl apply -f infra/k8s/ingress.yaml
```

## Cabotage env vars

Set these in the cabotage UI for the miss-islington application:

- `GH_SECRET` - GitHub webhook secret
- `GH_APP_ID` - GitHub App ID
- `GH_PRIVATE_KEY` - GitHub App private key
- `GH_AUTH` - GitHub auth token (used by the celery worker to clone cpython)
- `SENTRY_DSN` - Sentry DSN for error tracking
- `REDIS_URL` - printed by `generate-secrets.sh`, looks
like `rediss://:<password>@miss-islington.redis.svc.cluster.local:6379/0?ssl_ca_certs=/var/run/secrets/cabotage.io/ca.crt&ssl_cert_reqs=required`
40 changes: 40 additions & 0 deletions infra/k8s/cert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: miss-islington-certmanager
spec:
secretName: miss-islington-tls
duration: 2160h # 90d
renewBefore: 360h # 15d
subject:
organizations:
- miss-islington-org
commonName: miss-islington
isCA: false
privateKey:
algorithm: ECDSA
size: 256
usages:
- digital signature
- key encipherment
dnsNames:
- miss-islington
- miss-islington.redis
- miss-islington.redis.svc
- miss-islington.redis.svc.cluster.local
- miss-islington-0
- miss-islington-0.redis
- miss-islington-0.redis.svc
- miss-islington-0.redis.svc.cluster.local
- miss-islington-1
- miss-islington-1.redis
- miss-islington-1.redis.svc
- miss-islington-1.redis.svc.cluster.local
- miss-islington-2
- miss-islington-2.redis
- miss-islington-2.redis.svc
- miss-islington-2.redis.svc.cluster.local
issuerRef:
name: operators-ca-issuer
kind: ClusterIssuer
group: cert-manager.io
27 changes: 27 additions & 0 deletions infra/k8s/generate-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
# Generate Redis password secret for miss-islington
# Run once per cluster setup
set -e

NAMESPACE="redis"
SECRET_NAME="miss-islington-password"

echo "=== Miss Islington Redis Secret ==="

if kubectl get secret -n "$NAMESPACE" "$SECRET_NAME" &>/dev/null; then
echo "Secret '$SECRET_NAME' already exists in namespace '$NAMESPACE'."
echo "Delete it first if you want to regenerate: kubectl delete secret -n $NAMESPACE $SECRET_NAME"
exit 1
fi

PASSWORD=$(openssl rand -hex 24)

kubectl create secret -n "$NAMESPACE" generic "$SECRET_NAME" --from-literal=password="$PASSWORD"

echo ""
echo "Secret '$SECRET_NAME' created in namespace '$NAMESPACE'."
echo ""
echo "Redis connection URI (set as REDIS_URL in cabotage):"
echo "rediss://:${PASSWORD}@miss-islington.redis.svc.cluster.local:6379/0?ssl_ca_certs=/var/run/secrets/cabotage.io/ca.crt&ssl_cert_reqs=required"
echo ""
echo "=== Done ==="
42 changes: 42 additions & 0 deletions infra/k8s/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Miss Islington - Cabotage Ingress
# Apply manually: kubectl apply -f infra/k8s/ingress.yaml
#
# Exposes the Cabotage-deployed app via nginx-ingress
# with Let's Encrypt TLS certificates via cert-manager.
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: python-miss-islington-miss-islington-web
namespace: python
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-connect-timeout: 10s
nginx.ingress.kubernetes.io/proxy-next-upstream: error
nginx.ingress.kubernetes.io/proxy-read-timeout: 60s
nginx.ingress.kubernetes.io/proxy-request-buffering: "on"
nginx.ingress.kubernetes.io/proxy-send-timeout: 60s
nginx.ingress.kubernetes.io/service-upstream: "true"
labels:
app: python-miss-islington-miss-islington
process: web
resident-service.cabotage.io: "true"
spec:
ingressClassName: nginx
rules:
- host: python-miss-islington-miss-islington-web.ingress.us-east-2.psfhosted.computer
http:
paths:
- backend:
service:
name: python-miss-islington-miss-islington-web
port:
number: 443
path: /
pathType: Prefix
tls:
- hosts:
- python-miss-islington-miss-islington-web.ingress.us-east-2.psfhosted.computer
secretName: ingress-python-miss-islington-miss-islington-web
5 changes: 5 additions & 0 deletions infra/k8s/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace: redis

resources:
- cert.yaml
- redis.yaml
44 changes: 44 additions & 0 deletions infra/k8s/redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
apiVersion: redis.redis.opstreelabs.in/v1beta1
kind: Redis
metadata:
name: miss-islington
spec:
TLS:
secret:
secretName: miss-islington-tls
optional: false
securityContext:
runAsUser: 1000
fsGroup: 1000
kubernetesConfig:
image: quay.io/opstree/redis:v7.0.15
imagePullPolicy: IfNotPresent
redisSecret:
name: miss-islington-password
key: password
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 100m
memory: 128Mi
redisExporter:
enabled: false
image: quay.io/opstree/redis-exporter:v1.44.0
imagePullPolicy: Always
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 100m
memory: 256Mi
storage:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
24 changes: 16 additions & 8 deletions miss_islington/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ async def repo_installation_added(event, gh, *args, **kwargs):
print(f"App installed by {event.data['installation']['account']['login']}, installation_id: {event.data['installation']['id']}")


sentry_sdk.init(dsn=os.environ.get("SENTRY_DSN"), integrations=[AioHttpIntegration()])
app = web.Application()
app.router.add_post("/", main)
port = os.environ.get("PORT")
if port is not None:
port = int(port)

web.run_app(app, port=port)
async def health_check(request):
"""Health check endpoint for container orchestration."""
return web.Response(status=200, text="OK")


if __name__ == "__main__": # pragma: no cover
sentry_sdk.init(dsn=os.environ.get("SENTRY_DSN"), integrations=[AioHttpIntegration()])
app = web.Application()
app.router.add_post("/", main)
app.router.add_get("/health", health_check)

if os.path.isdir("/var/run/cabotage"):
web.run_app(app, path="/var/run/cabotage/cabotage.sock")
else:
port = os.environ.get("PORT")
web.run_app(app, port=int(port) if port else None)
4 changes: 2 additions & 2 deletions miss_islington/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
app = celery.Celery("backport_cpython")

app.conf.update(
broker_url=os.environ["HEROKU_REDIS_MAROON_URL"],
result_backend=os.environ["HEROKU_REDIS_MAROON_URL"],
broker_url=os.environ.get("REDIS_URL", os.environ.get("HEROKU_REDIS_MAROON_URL", "")),
result_backend=os.environ.get("REDIS_URL", os.environ.get("HEROKU_REDIS_MAROON_URL", "")),
broker_connection_retry_on_startup=True,
broker_use_ssl={"ssl_cert_reqs": ssl.CERT_NONE},
redis_backend_use_ssl={"ssl_cert_reqs": ssl.CERT_NONE},
Expand Down
1 change: 0 additions & 1 deletion runtime.txt

This file was deleted.

2 changes: 1 addition & 1 deletion tests/test_backport_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import redis
import kombu

os.environ["HEROKU_REDIS_MAROON_URL"] = "someurl"
os.environ.setdefault("REDIS_URL", "redis://localhost")

from miss_islington import backport_pr

Expand Down
2 changes: 1 addition & 1 deletion tests/test_delete_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from gidgethub import sansio
import pytest

os.environ.setdefault("HEROKU_REDIS_MAROON_URL", "someurl")
os.environ.setdefault("REDIS_URL", "redis://localhost")

from miss_islington import delete_branch, tasks

Expand Down
Loading