diff --git a/ansible/roles/devpi/tasks/main.yml b/ansible/roles/devpi/tasks/main.yml index 72e7853..985ca46 100644 --- a/ansible/roles/devpi/tasks/main.yml +++ b/ansible/roles/devpi/tasks/main.yml @@ -23,10 +23,13 @@ creates: "{{ devpi_venv }}/bin/python" - name: Install devpi-server and devpi-web into venv + # Always bootstrap from upstream PyPI — devpi is the index it would otherwise resolve through, + # and that's a circular dependency (devpi cannot install itself from itself). ansible.builtin.command: cmd: >- {{ devpi_uv_binary }} pip install --python {{ devpi_venv }}/bin/python + --index-url https://pypi.org/simple/ devpi-server=={{ devpi_server_version }} devpi-web=={{ devpi_web_version }} register: devpi_pip_install @@ -45,6 +48,7 @@ --serverdir {{ devpi_server_dir }} --root-passwd {{ devpi_root_password }} when: not devpi_serverversion.stat.exists + changed_when: true no_log: true - name: Deploy devpi LaunchAgent plist diff --git a/argocd/apps/devpi.yaml b/argocd/apps/devpi.yaml deleted file mode 100644 index 4a15672..0000000 --- a/argocd/apps/devpi.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# 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 -# 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@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/devpi - destination: - server: https://kubernetes.default.svc - namespace: devpi - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/manifests/devpi/README.md b/argocd/manifests/devpi/README.md deleted file mode 100644 index 11fd697..0000000 --- a/argocd/manifests/devpi/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# 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 diff --git a/argocd/manifests/devpi/external-secret.yaml b/argocd/manifests/devpi/external-secret.yaml deleted file mode 100644 index 290ea67..0000000 --- a/argocd/manifests/devpi/external-secret.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# ExternalSecret for devpi root password -# -# Replaces the manual op inject workflow from secret-root.yaml.tpl -# -# 1Password item: "devpi" in blumeops vault -# Field: "root password" -# -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: devpi-root - namespace: devpi -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: devpi-root - creationPolicy: Owner - data: - - secretKey: password - remoteRef: - key: devpi - property: root password diff --git a/argocd/manifests/devpi/ingress-tailscale.yaml b/argocd/manifests/devpi/ingress-tailscale.yaml deleted file mode 100644 index 474bf72..0000000 --- a/argocd/manifests/devpi/ingress-tailscale.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: devpi-tailscale - namespace: devpi - annotations: - tailscale.com/proxy-class: "default" - tailscale.com/proxy-group: "ingress" - gethomepage.dev/enabled: "true" - gethomepage.dev/name: "PyPI" - gethomepage.dev/group: "Infrastructure" - gethomepage.dev/icon: "pypi.png" - gethomepage.dev/description: "PyPI cache" - gethomepage.dev/href: "https://pypi.ops.eblu.me" - gethomepage.dev/pod-selector: "app=devpi" -spec: - ingressClassName: tailscale - defaultBackend: - service: - name: devpi - port: - number: 3141 - tls: - - hosts: - - pypi diff --git a/argocd/manifests/devpi/kustomization.yaml b/argocd/manifests/devpi/kustomization.yaml deleted file mode 100644 index 2083aaa..0000000 --- a/argocd/manifests/devpi/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: devpi - -resources: - - statefulset.yaml - - service.yaml - - ingress-tailscale.yaml - - external-secret.yaml - -images: - - name: registry.ops.eblu.me/blumeops/devpi - newTag: v6.19.3-37b8a21 diff --git a/argocd/manifests/devpi/service.yaml b/argocd/manifests/devpi/service.yaml deleted file mode 100644 index 42e1543..0000000 --- a/argocd/manifests/devpi/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: devpi - namespace: devpi -spec: - selector: - app: devpi - ports: - - name: http - port: 3141 - targetPort: 3141 - protocol: TCP diff --git a/argocd/manifests/devpi/statefulset.yaml b/argocd/manifests/devpi/statefulset.yaml deleted file mode 100644 index 91875df..0000000 --- a/argocd/manifests/devpi/statefulset.yaml +++ /dev/null @@ -1,64 +0,0 @@ -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 - seccompProfile: - type: RuntimeDefault - containers: - - name: devpi - image: registry.ops.eblu.me/blumeops/devpi:kustomized - env: - - name: DEVPI_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: devpi-root - key: password - - name: DEVPI_OUTSIDE_URL - value: "https://pypi.ops.eblu.me" - 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 diff --git a/docs/changelog.d/migrate-devpi-to-indri.infra.md b/docs/changelog.d/migrate-devpi-to-indri.infra.md new file mode 100644 index 0000000..418db70 --- /dev/null +++ b/docs/changelog.d/migrate-devpi-to-indri.infra.md @@ -0,0 +1 @@ +Migrated devpi (PyPI mirror at `pypi.ops.eblu.me`) from a minikube StatefulSet to a launchd-managed service on indri. devpi-server now runs in a uv-managed venv with pinned `devpi-server` and `devpi-web` versions, listens on `127.0.0.1:3141`, and is fronted by Caddy. The minikube StatefulSet was crash-looping under memory pressure (and breaking the Python toolchain everywhere); the new layout removes a layer of dependency on cluster health for critical-path tooling. See [[devpi-on-indri]]. diff --git a/docs/how-to/operations/restart-indri.md b/docs/how-to/operations/restart-indri.md index a956644..e92581e 100644 --- a/docs/how-to/operations/restart-indri.md +++ b/docs/how-to/operations/restart-indri.md @@ -41,6 +41,7 @@ Native services managed by launchd will stop automatically during macOS shutdown ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.caddy.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.zot.plist' +ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi.plist' # see [[devpi-on-indri]] ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.jellyfin.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.alloy.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.borgmatic.plist' diff --git a/service-versions.yaml b/service-versions.yaml index 0a4fe93..e819c6c 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -214,10 +214,11 @@ services: upstream-source: https://github.com/kiwix/kiwix-tools/releases - name: devpi - type: argocd - last-reviewed: 2026-04-18 + type: ansible + last-reviewed: 2026-04-29 current-version: "6.19.3" upstream-source: https://github.com/devpi/devpi/releases + notes: Installed via uv into a venv on indri; version pinned in ansible/roles/devpi/defaults/main.yml - name: cv type: argocd