P5: Migrate devpi to Kubernetes (#34)
## Summary - Migrate devpi PyPI caching proxy from indri LaunchAgent to Kubernetes - Custom container image with devpi-server + devpi-web + auto-init - StatefulSet with 50Gi PVC, Tailscale Ingress at pypi.tail8d86e.ts.net - Remove devpi from ansible playbooks and update CLAUDE.md with k8s workflow ## Key Changes - Add CRI-O registry mirror config for registry.tail8d86e.ts.net - Change ArgoCD apps to manual sync (was auto-sync causing issues) - 2Gi memory limit for Whoosh indexer (reclaimed after startup) ## Deployment and Testing - [x] devpi pod healthy in k8s - [x] pip install through proxy works - [x] mcquack 1.0.0 uploaded and installable - [x] Old devpi stopped on indri ## Post-Merge Reset ArgoCD to main: ``` argocd app set apps --revision main && argocd app sync apps argocd app set devpi --revision main && argocd app sync devpi ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/34
This commit is contained in:
parent
b2307412fc
commit
0439fbb704
18 changed files with 474 additions and 75 deletions
|
|
@ -15,9 +15,7 @@ spec:
|
|||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
# selfHeal disabled: allows manual revision changes on child apps during development
|
||||
# Sync apps app manually when adding/removing Application manifests
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
# Manual sync only - no automated sync on git push
|
||||
# To pick up new apps: argocd app sync apps
|
||||
|
|
|
|||
30
argocd/apps/devpi.yaml
Normal file
30
argocd/apps/devpi.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# devpi PyPI Caching Proxy
|
||||
# Provides PyPI cache and private package hosting
|
||||
#
|
||||
# After first deployment, initialize devpi:
|
||||
# kubectl -n devpi exec -it devpi-0 -- devpi-init --serverdir /devpi --root-passwd <password>
|
||||
# kubectl -n devpi rollout restart statefulset devpi
|
||||
#
|
||||
# Then create user/index:
|
||||
# uvx devpi use https://pypi.tail8d86e.ts.net
|
||||
# uvx devpi login root
|
||||
# uvx devpi user -c eblume email=blume.erich@gmail.com
|
||||
# uvx devpi index -c eblume/dev bases=root/pypi
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: devpi
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/devpi
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: devpi
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
# Manual sync only - no automated sync on git push
|
||||
19
argocd/manifests/devpi/Dockerfile
Normal file
19
argocd/manifests/devpi/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
# Install devpi-server and devpi-web
|
||||
RUN pip install --no-cache-dir devpi-server devpi-web
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -r -u 1000 devpi && mkdir -p /devpi && chown devpi:devpi /devpi
|
||||
|
||||
# Add startup script
|
||||
COPY --chown=devpi:devpi start.sh /usr/local/bin/start.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
USER devpi
|
||||
WORKDIR /devpi
|
||||
|
||||
# Expose default port
|
||||
EXPOSE 3141
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/start.sh"]
|
||||
72
argocd/manifests/devpi/README.md
Normal file
72
argocd/manifests/devpi/README.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# devpi PyPI Caching Proxy
|
||||
|
||||
devpi-server running in Kubernetes, providing:
|
||||
- PyPI caching proxy at `root/pypi`
|
||||
- Private package hosting at `eblume/dev`
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create the root password secret
|
||||
|
||||
```fish
|
||||
kubectl create namespace devpi
|
||||
op inject -i argocd/manifests/devpi/secret-root.yaml.tpl | kubectl apply -f -
|
||||
```
|
||||
|
||||
### 2. Deploy via ArgoCD
|
||||
|
||||
```fish
|
||||
argocd app sync apps
|
||||
argocd app sync devpi
|
||||
```
|
||||
|
||||
The container will auto-initialize on first startup using the root password from the secret.
|
||||
|
||||
### 3. Create user and index (first time only)
|
||||
|
||||
After the pod is running:
|
||||
|
||||
```fish
|
||||
# Login to devpi as root
|
||||
uvx --from devpi-client devpi use https://pypi.tail8d86e.ts.net
|
||||
uvx --from devpi-client devpi login root
|
||||
# Enter root password when prompted
|
||||
|
||||
# Create eblume user (prompts for password - use the one from 1Password)
|
||||
uvx --from devpi-client devpi user -c eblume email=blume.erich@gmail.com
|
||||
|
||||
# Create private index inheriting from PyPI
|
||||
uvx --from devpi-client devpi index -c eblume/dev bases=root/pypi
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As pip index (caching proxy)
|
||||
|
||||
Configure `~/.config/pip/pip.conf`:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
index-url = https://pypi.tail8d86e.ts.net/root/pypi/+simple/
|
||||
trusted-host = pypi.tail8d86e.ts.net
|
||||
```
|
||||
|
||||
### Upload private packages
|
||||
|
||||
```fish
|
||||
cd ~/code/personal/your-package
|
||||
uv build
|
||||
uv publish --publish-url https://pypi.tail8d86e.ts.net/eblume/dev/
|
||||
```
|
||||
|
||||
## URLs
|
||||
|
||||
- Web UI: https://pypi.tail8d86e.ts.net
|
||||
- PyPI cache: https://pypi.tail8d86e.ts.net/root/pypi/+simple/
|
||||
- Private index: https://pypi.tail8d86e.ts.net/eblume/dev/+simple/
|
||||
|
||||
## Credentials
|
||||
|
||||
Stored in 1Password vault `blumeops`, item `kyhzfifryqnuk7jeyibmmjvxxm`:
|
||||
- `root password` - devpi root user
|
||||
- `password` - eblume user password
|
||||
17
argocd/manifests/devpi/ingress-tailscale.yaml
Normal file
17
argocd/manifests/devpi/ingress-tailscale.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: devpi-tailscale
|
||||
namespace: devpi
|
||||
annotations:
|
||||
tailscale.com/proxy-class: "crio-compat"
|
||||
spec:
|
||||
ingressClassName: tailscale
|
||||
defaultBackend:
|
||||
service:
|
||||
name: devpi
|
||||
port:
|
||||
number: 3141
|
||||
tls:
|
||||
- hosts:
|
||||
- pypi
|
||||
9
argocd/manifests/devpi/kustomization.yaml
Normal file
9
argocd/manifests/devpi/kustomization.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: devpi
|
||||
|
||||
resources:
|
||||
- statefulset.yaml
|
||||
- service.yaml
|
||||
- ingress-tailscale.yaml
|
||||
12
argocd/manifests/devpi/secret-root.yaml.tpl
Normal file
12
argocd/manifests/devpi/secret-root.yaml.tpl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Template for devpi root password secret
|
||||
# Create the secret before deploying:
|
||||
# kubectl create namespace devpi
|
||||
# op inject -i argocd/manifests/devpi/secret-root.yaml.tpl | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: devpi-root
|
||||
namespace: devpi
|
||||
type: Opaque
|
||||
stringData:
|
||||
password: "{{ op://vg6xf6vvfmoh5hqjjhlhbeoaie/kyhzfifryqnuk7jeyibmmjvxxm/root password }}"
|
||||
13
argocd/manifests/devpi/service.yaml
Normal file
13
argocd/manifests/devpi/service.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: devpi
|
||||
namespace: devpi
|
||||
spec:
|
||||
selector:
|
||||
app: devpi
|
||||
ports:
|
||||
- name: http
|
||||
port: 3141
|
||||
targetPort: 3141
|
||||
protocol: TCP
|
||||
31
argocd/manifests/devpi/start.sh
Normal file
31
argocd/manifests/devpi/start.sh
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SERVERDIR="${DEVPI_SERVERDIR:-/devpi}"
|
||||
HOST="${DEVPI_HOST:-0.0.0.0}"
|
||||
# Note: Can't use DEVPI_PORT - Kubernetes auto-sets it for service discovery
|
||||
PORT="${DEVPI_LISTEN_PORT:-3141}"
|
||||
OUTSIDE_URL="${DEVPI_OUTSIDE_URL:-}"
|
||||
|
||||
# Check if devpi is initialized
|
||||
if [ ! -f "$SERVERDIR/.serverversion" ]; then
|
||||
echo "Initializing devpi server..."
|
||||
|
||||
if [ -z "$DEVPI_ROOT_PASSWORD" ]; then
|
||||
echo "ERROR: DEVPI_ROOT_PASSWORD environment variable must be set for initialization"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
devpi-init --serverdir "$SERVERDIR" --root-passwd "$DEVPI_ROOT_PASSWORD"
|
||||
echo "Devpi initialized successfully"
|
||||
fi
|
||||
|
||||
# Build command
|
||||
CMD="devpi-server --serverdir $SERVERDIR --host $HOST --port $PORT"
|
||||
|
||||
if [ -n "$OUTSIDE_URL" ]; then
|
||||
CMD="$CMD --outside-url $OUTSIDE_URL"
|
||||
fi
|
||||
|
||||
echo "Starting devpi-server..."
|
||||
exec $CMD
|
||||
62
argocd/manifests/devpi/statefulset.yaml
Normal file
62
argocd/manifests/devpi/statefulset.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: devpi
|
||||
namespace: devpi
|
||||
spec:
|
||||
serviceName: devpi
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: devpi
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: devpi
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: devpi
|
||||
image: registry.tail8d86e.ts.net/blumeops/devpi:latest
|
||||
env:
|
||||
- name: DEVPI_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: devpi-root
|
||||
key: password
|
||||
- name: DEVPI_OUTSIDE_URL
|
||||
value: "https://pypi.tail8d86e.ts.net"
|
||||
ports:
|
||||
- containerPort: 3141
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /devpi
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "2Gi" # High limit for initial PyPI index build, reclaimed after
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /+api
|
||||
port: 3141
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /+api
|
||||
port: 3141
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
Loading…
Add table
Add a link
Reference in a new issue