P5.1: Migrate minikube from podman to QEMU2 driver (#38)
## Summary - Migrate minikube from podman driver to qemu2 driver for proper NFS/SMB volume mount support - Update ansible minikube role with qemu installation and containerd runtime - Remove podman role dependency from indri.yml - Add synology user creation steps and post-migration zot reconfiguration notes ## Why Phase 6 (Kiwix/Transmission migration) was blocked because the podman driver lacks kernel capabilities for filesystem mounts. QEMU2 creates an actual VM with full mount support. ## Deployment and Testing - [ ] Create k8s-storage user on Synology DSM - [ ] Store credentials in 1Password (synology-k8s-storage) - [ ] Export current k8s state - [ ] Stop and delete podman-based minikube cluster - [ ] Run ansible to create QEMU2 cluster - [ ] Test NFS volume mount with test pod - [ ] Redeploy ArgoCD and all apps - [ ] Verify all services healthy - [ ] Reconfigure zot registry mirrors for containerd (post-migration) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/38
This commit is contained in:
parent
7b60cca31e
commit
21848a7919
20 changed files with 490 additions and 542 deletions
|
|
@ -47,8 +47,6 @@
|
||||||
tags: zot
|
tags: zot
|
||||||
- role: zot_metrics
|
- role: zot_metrics
|
||||||
tags: zot_metrics
|
tags: zot_metrics
|
||||||
- role: podman
|
|
||||||
tags: podman
|
|
||||||
- role: minikube
|
- role: minikube
|
||||||
tags: minikube
|
tags: minikube
|
||||||
- role: minikube_metrics
|
- role: minikube_metrics
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
---
|
---
|
||||||
# Minikube cluster configuration
|
# Minikube cluster configuration
|
||||||
minikube_cpus: 4
|
# Uses docker driver - requires Docker Desktop to be installed and running
|
||||||
# Note: Must be less than podman machine memory (8192MB) to account for overhead
|
# with at least 12GB memory allocated in Docker Desktop settings
|
||||||
minikube_memory: 7800
|
minikube_cpus: 6
|
||||||
|
minikube_memory: 11264 # Leave ~1GB headroom for Docker Desktop overhead
|
||||||
minikube_disk_size: "200g"
|
minikube_disk_size: "200g"
|
||||||
minikube_driver: podman
|
minikube_driver: docker
|
||||||
minikube_container_runtime: cri-o
|
minikube_container_runtime: docker
|
||||||
|
|
||||||
# Remote access configuration
|
# Remote access configuration
|
||||||
# These allow kubectl from other machines (e.g., gilbert) to connect
|
# These allow kubectl from other machines (e.g., gilbert) to connect
|
||||||
# k8s.tail8d86e.ts.net is exposed via Tailscale service (TCP passthrough)
|
# k8s.tail8d86e.ts.net is exposed via Tailscale service (TCP passthrough to localhost)
|
||||||
minikube_apiserver_names:
|
minikube_apiserver_names:
|
||||||
- k8s.tail8d86e.ts.net
|
- k8s.tail8d86e.ts.net
|
||||||
- indri
|
- indri
|
||||||
# Note: apiserver_port is the INTERNAL container port; with podman driver,
|
|
||||||
# the host port is dynamically assigned. Check actual port with:
|
|
||||||
# kubectl config view --minify -o jsonpath="{.clusters[0].cluster.server}"
|
|
||||||
minikube_apiserver_port: 6443
|
minikube_apiserver_port: 6443
|
||||||
minikube_listen_address: "0.0.0.0"
|
minikube_listen_address: "0.0.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# Zot pull-through cache on indri
|
|
||||||
# Uses host.containers.internal which is stable across restarts
|
|
||||||
# Applied by ansible minikube role
|
|
||||||
|
|
||||||
# Direct access to Zot for private images (blumeops/*)
|
|
||||||
[[registry]]
|
|
||||||
prefix = "host.containers.internal:5050"
|
|
||||||
location = "host.containers.internal:5050"
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
# Tailscale hostname for Zot - redirects to local access
|
|
||||||
# Allows manifests to use registry.tail8d86e.ts.net which is cleaner
|
|
||||||
[[registry]]
|
|
||||||
prefix = "registry.tail8d86e.ts.net"
|
|
||||||
location = "registry.tail8d86e.ts.net"
|
|
||||||
|
|
||||||
[[registry.mirror]]
|
|
||||||
location = "host.containers.internal:5050"
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[[registry]]
|
|
||||||
prefix = "docker.io"
|
|
||||||
location = "docker.io"
|
|
||||||
|
|
||||||
[[registry.mirror]]
|
|
||||||
location = "host.containers.internal:5050/docker.io"
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[[registry]]
|
|
||||||
prefix = "ghcr.io"
|
|
||||||
location = "ghcr.io"
|
|
||||||
|
|
||||||
[[registry.mirror]]
|
|
||||||
location = "host.containers.internal:5050/ghcr.io"
|
|
||||||
insecure = true
|
|
||||||
|
|
||||||
[[registry]]
|
|
||||||
prefix = "quay.io"
|
|
||||||
location = "quay.io"
|
|
||||||
|
|
||||||
[[registry.mirror]]
|
|
||||||
location = "host.containers.internal:5050/quay.io"
|
|
||||||
insecure = true
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
minikube start
|
minikube start
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Restart CRI-O in minikube
|
- name: Restart containerd in minikube
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: minikube ssh --native-ssh=false "sudo systemctl restart crio"
|
cmd: minikube ssh --native-ssh=false "sudo systemctl restart containerd"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
---
|
---
|
||||||
# Minikube installation and cluster setup for indri
|
# Minikube installation and cluster setup for indri
|
||||||
# Requires podman machine to be running (see podman role)
|
# Uses docker driver - requires Docker Desktop to be installed manually first
|
||||||
|
# (Docker Desktop requires GUI setup, so it's not automated in this role)
|
||||||
#
|
#
|
||||||
# NOTE: Similar to podman, minikube start may have issues when run via SSH.
|
# Prerequisites:
|
||||||
|
# 1. Install Docker Desktop: brew install --cask docker
|
||||||
|
# 2. Launch Docker Desktop and complete setup wizard
|
||||||
|
# 3. Configure Docker Desktop with at least 12GB memory
|
||||||
|
#
|
||||||
|
# NOTE: minikube start may have issues when run via SSH.
|
||||||
# If cluster fails to start, manually run on indri:
|
# If cluster fails to start, manually run on indri:
|
||||||
# minikube start --driver=podman --container-runtime=cri-o \
|
# minikube start --driver=docker --container-runtime=docker \
|
||||||
# --cpus=4 --memory=7800 --disk-size=200g \
|
# --cpus=6 --memory=11264 --disk-size=200g \
|
||||||
# --apiserver-names=k8s.tail8d86e.ts.net --apiserver-names=indri \
|
# --apiserver-names=k8s.tail8d86e.ts.net --apiserver-names=indri \
|
||||||
# --apiserver-port=6443 --listen-address=0.0.0.0
|
# --apiserver-port=6443 --listen-address=0.0.0.0
|
||||||
|
|
||||||
|
|
@ -19,6 +25,18 @@
|
||||||
name: kubectl
|
name: kubectl
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
- name: Check if Docker is running
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: docker info
|
||||||
|
register: minikube_docker_status
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Warn if Docker is not running
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "WARNING: Docker does not appear to be running. Please start Docker Desktop manually."
|
||||||
|
when: minikube_docker_status.rc != 0
|
||||||
|
|
||||||
- name: Check if minikube cluster exists
|
- name: Check if minikube cluster exists
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: minikube status --format={% raw %}'{{.Host}}'{% endraw %}
|
cmd: minikube status --format={% raw %}'{{.Host}}'{% endraw %}
|
||||||
|
|
@ -42,8 +60,10 @@
|
||||||
--listen-address={{ minikube_listen_address }}
|
--listen-address={{ minikube_listen_address }}
|
||||||
register: minikube_start
|
register: minikube_start
|
||||||
changed_when: minikube_start.rc == 0
|
changed_when: minikube_start.rc == 0
|
||||||
failed_when: false # Don't fail - may need manual intervention like podman
|
failed_when: false # Don't fail - may need manual intervention
|
||||||
when: minikube_status.rc != 0 or 'Running' not in minikube_status.stdout
|
when:
|
||||||
|
- minikube_docker_status.rc == 0
|
||||||
|
- minikube_status.rc != 0 or 'Running' not in minikube_status.stdout
|
||||||
|
|
||||||
- name: Check minikube status after start attempt
|
- name: Check minikube status after start attempt
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
|
|
@ -57,54 +77,146 @@
|
||||||
msg: "WARNING: minikube may not have started properly. Run 'minikube start' manually on indri if needed. Status: {{ minikube_final_status.stdout | default('unknown') }}"
|
msg: "WARNING: minikube may not have started properly. Run 'minikube start' manually on indri if needed. Status: {{ minikube_final_status.stdout | default('unknown') }}"
|
||||||
when: minikube_final_status.rc != 0 or 'Running' not in minikube_final_status.stdout
|
when: minikube_final_status.rc != 0 or 'Running' not in minikube_final_status.stdout
|
||||||
|
|
||||||
# Configure CRI-O to use zot as pull-through cache
|
# Configure containerd to use zot registry as pull-through cache
|
||||||
- name: Read desired zot mirror config
|
# With docker driver, use host.minikube.internal to reach the host
|
||||||
ansible.builtin.slurp:
|
# Zot runs on indri:5050 and caches images from docker.io, ghcr.io, quay.io
|
||||||
src: "{{ role_path }}/files/zot-mirror.conf"
|
|
||||||
register: minikube_desired_zot_config
|
|
||||||
delegate_to: localhost
|
|
||||||
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
|
||||||
|
|
||||||
- name: Check current zot mirror config in minikube
|
- name: Create containerd registry mirror directories
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: minikube ssh --native-ssh=false "cat /etc/containers/registries.conf.d/zot-mirror.conf 2>/dev/null || echo ''"
|
cmd: minikube ssh --native-ssh=false "sudo mkdir -p /etc/containerd/certs.d/{{ item }}"
|
||||||
register: minikube_existing_zot_config
|
loop:
|
||||||
|
- registry.tail8d86e.ts.net
|
||||||
|
- docker.io
|
||||||
|
- ghcr.io
|
||||||
|
- quay.io
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
- name: Determine if zot mirror config needs update
|
# Private registry (registry.tail8d86e.ts.net) - direct to zot
|
||||||
ansible.builtin.set_fact:
|
- name: Check registry.tail8d86e.ts.net config
|
||||||
minikube_zot_config_changed: "{{ (minikube_existing_zot_config.stdout | trim) != (minikube_desired_zot_config.content | b64decode | trim) }}"
|
ansible.builtin.command:
|
||||||
|
cmd: minikube ssh --native-ssh=false "cat /etc/containerd/certs.d/registry.tail8d86e.ts.net/hosts.toml 2>/dev/null || echo ''"
|
||||||
|
register: minikube_registry_config
|
||||||
|
changed_when: false
|
||||||
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
- name: Copy zot mirror config to temp location
|
- name: Configure registry.tail8d86e.ts.net mirror
|
||||||
ansible.builtin.copy:
|
ansible.builtin.command:
|
||||||
src: zot-mirror.conf
|
|
||||||
dest: /tmp/zot-mirror.conf
|
|
||||||
mode: "0644"
|
|
||||||
when:
|
|
||||||
- minikube_final_status.rc == 0
|
|
||||||
- "'Running' in minikube_final_status.stdout"
|
|
||||||
- minikube_zot_config_changed | default(false)
|
|
||||||
|
|
||||||
- name: Apply zot mirror config to minikube
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: |
|
cmd: |
|
||||||
set -o pipefail
|
minikube ssh --native-ssh=false 'echo "server = \"http://host.minikube.internal:5050\"
|
||||||
cat /tmp/zot-mirror.conf | minikube ssh --native-ssh=false "sudo tee /etc/containers/registries.conf.d/zot-mirror.conf > /dev/null"
|
|
||||||
executable: /bin/bash
|
|
||||||
changed_when: true # Task only runs when config needs updating
|
|
||||||
when:
|
|
||||||
- minikube_final_status.rc == 0
|
|
||||||
- "'Running' in minikube_final_status.stdout"
|
|
||||||
- minikube_zot_config_changed | default(false)
|
|
||||||
notify: Restart CRI-O in minikube
|
|
||||||
|
|
||||||
- name: Clean up temp config file
|
[host.\"http://host.minikube.internal:5050\"]
|
||||||
ansible.builtin.file:
|
capabilities = [\"pull\", \"resolve\", \"push\"]
|
||||||
path: /tmp/zot-mirror.conf
|
skip_verify = true" | sudo tee /etc/containerd/certs.d/registry.tail8d86e.ts.net/hosts.toml'
|
||||||
state: absent
|
changed_when: true
|
||||||
when:
|
when:
|
||||||
- minikube_final_status.rc == 0
|
- minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
- "'Running' in minikube_final_status.stdout"
|
- "'host.minikube.internal:5050' not in minikube_registry_config.stdout"
|
||||||
- minikube_zot_config_changed | default(false)
|
notify: Restart containerd in minikube
|
||||||
|
|
||||||
|
# Docker Hub (docker.io) - zot pull-through cache
|
||||||
|
- name: Check docker.io config
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: minikube ssh --native-ssh=false "cat /etc/containerd/certs.d/docker.io/hosts.toml 2>/dev/null || echo ''"
|
||||||
|
register: minikube_dockerio_config
|
||||||
|
changed_when: false
|
||||||
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
|
- name: Configure docker.io mirror through zot
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: |
|
||||||
|
minikube ssh --native-ssh=false 'echo "server = \"https://registry-1.docker.io\"
|
||||||
|
|
||||||
|
[host.\"http://host.minikube.internal:5050\"]
|
||||||
|
capabilities = [\"pull\", \"resolve\"]
|
||||||
|
skip_verify = true" | sudo tee /etc/containerd/certs.d/docker.io/hosts.toml'
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
- "'host.minikube.internal:5050' not in minikube_dockerio_config.stdout"
|
||||||
|
notify: Restart containerd in minikube
|
||||||
|
|
||||||
|
# GitHub Container Registry (ghcr.io) - zot pull-through cache
|
||||||
|
- name: Check ghcr.io config
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: minikube ssh --native-ssh=false "cat /etc/containerd/certs.d/ghcr.io/hosts.toml 2>/dev/null || echo ''"
|
||||||
|
register: minikube_ghcr_config
|
||||||
|
changed_when: false
|
||||||
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
|
- name: Configure ghcr.io mirror through zot
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: |
|
||||||
|
minikube ssh --native-ssh=false 'echo "server = \"https://ghcr.io\"
|
||||||
|
|
||||||
|
[host.\"http://host.minikube.internal:5050\"]
|
||||||
|
capabilities = [\"pull\", \"resolve\"]
|
||||||
|
skip_verify = true" | sudo tee /etc/containerd/certs.d/ghcr.io/hosts.toml'
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
- "'host.minikube.internal:5050' not in minikube_ghcr_config.stdout"
|
||||||
|
notify: Restart containerd in minikube
|
||||||
|
|
||||||
|
# Quay.io - zot pull-through cache
|
||||||
|
- name: Check quay.io config
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: minikube ssh --native-ssh=false "cat /etc/containerd/certs.d/quay.io/hosts.toml 2>/dev/null || echo ''"
|
||||||
|
register: minikube_quay_config
|
||||||
|
changed_when: false
|
||||||
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
|
- name: Configure quay.io mirror through zot
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: |
|
||||||
|
minikube ssh --native-ssh=false 'echo "server = \"https://quay.io\"
|
||||||
|
|
||||||
|
[host.\"http://host.minikube.internal:5050\"]
|
||||||
|
capabilities = [\"pull\", \"resolve\"]
|
||||||
|
skip_verify = true" | sudo tee /etc/containerd/certs.d/quay.io/hosts.toml'
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
- "'host.minikube.internal:5050' not in minikube_quay_config.stdout"
|
||||||
|
notify: Restart containerd in minikube
|
||||||
|
|
||||||
|
# Configure Tailscale serve for k8s API access
|
||||||
|
# With docker driver, the API server port is dynamic (not fixed at 6443)
|
||||||
|
# We query the current port and configure tailscale serve accordingly
|
||||||
|
- name: Get minikube API server URL
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: kubectl config view --minify -o jsonpath="{.clusters[0].cluster.server}"
|
||||||
|
register: minikube_api_url
|
||||||
|
changed_when: false
|
||||||
|
when: minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
|
||||||
|
- name: Extract API server port from URL
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
minikube_api_port: "{{ minikube_api_url.stdout | regex_search(':([0-9]+)$', '\\1') | first }}"
|
||||||
|
when:
|
||||||
|
- minikube_final_status.rc == 0 and 'Running' in minikube_final_status.stdout
|
||||||
|
- minikube_api_url.stdout is defined
|
||||||
|
|
||||||
|
- name: Check current tailscale serve config for k8s
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: tailscale serve status --json
|
||||||
|
register: minikube_tailscale_serve_status
|
||||||
|
changed_when: false
|
||||||
|
when: minikube_api_port is defined
|
||||||
|
|
||||||
|
- name: Parse tailscale serve k8s config
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
minikube_tailscale_k8s_tcp: "{{ ((minikube_tailscale_serve_status.stdout | from_json).Services['svc:k8s'].TCP['443'].TCPForward | default('')) }}"
|
||||||
|
when:
|
||||||
|
- minikube_api_port is defined
|
||||||
|
- minikube_tailscale_serve_status.stdout is defined
|
||||||
|
- "'svc:k8s' in (minikube_tailscale_serve_status.stdout | from_json).Services | default({})"
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Configure tailscale serve for k8s API
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: tailscale serve --service="svc:k8s" --tcp=443 tcp://localhost:{{ minikube_api_port }}
|
||||||
|
when:
|
||||||
|
- minikube_api_port is defined
|
||||||
|
- minikube_tailscale_k8s_tcp is not defined or minikube_tailscale_k8s_tcp != 'localhost:' + minikube_api_port
|
||||||
|
changed_when: true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
tailscale_serve_services:
|
tailscale_serve_services:
|
||||||
# NOTE: svc:grafana, svc:pg, svc:feed, svc:pypi removed - now hosted in k8s
|
# NOTE: svc:grafana, svc:pg, svc:feed, svc:pypi removed - now hosted in k8s
|
||||||
|
# NOTE: svc:k8s is configured by the minikube role (port is dynamic with docker driver)
|
||||||
|
|
||||||
- name: svc:forge
|
- name: svc:forge
|
||||||
https:
|
https:
|
||||||
|
|
@ -22,11 +23,3 @@ tailscale_serve_services:
|
||||||
https:
|
https:
|
||||||
port: 443
|
port: 443
|
||||||
upstream: http://localhost:5050
|
upstream: http://localhost:5050
|
||||||
|
|
||||||
# Kubernetes API server (TCP passthrough for mTLS)
|
|
||||||
# NOTE: Port is dynamic with podman driver - check with:
|
|
||||||
# ssh indri "kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'"
|
|
||||||
- name: svc:k8s
|
|
||||||
tcp:
|
|
||||||
port: 443
|
|
||||||
upstream: tcp://localhost:44491
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ metadata:
|
||||||
name: argocd-server-tailscale
|
name: argocd-server-tailscale
|
||||||
namespace: argocd
|
namespace: argocd
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: tailscale
|
ingressClassName: tailscale
|
||||||
defaultBackend:
|
defaultBackend:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ metadata:
|
||||||
namespace: databases
|
namespace: databases
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/hostname: "pg"
|
tailscale.com/hostname: "pg"
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
type: LoadBalancer
|
type: LoadBalancer
|
||||||
loadBalancerClass: tailscale
|
loadBalancerClass: tailscale
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ metadata:
|
||||||
name: devpi-tailscale
|
name: devpi-tailscale
|
||||||
namespace: devpi
|
namespace: devpi
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: tailscale
|
ingressClassName: tailscale
|
||||||
defaultBackend:
|
defaultBackend:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ metadata:
|
||||||
name: grafana-tailscale
|
name: grafana-tailscale
|
||||||
namespace: monitoring
|
namespace: monitoring
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: tailscale
|
ingressClassName: tailscale
|
||||||
defaultBackend:
|
defaultBackend:
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,13 @@ Connects to PostgreSQL via internal k8s DNS:
|
||||||
|
|
||||||
The database is also accessible externally via Tailscale at:
|
The database is also accessible externally via Tailscale at:
|
||||||
`pg.tail8d86e.ts.net:5432`
|
`pg.tail8d86e.ts.net:5432`
|
||||||
|
|
||||||
|
## Restore from Backup
|
||||||
|
|
||||||
|
If the database needs to be restored from a borgmatic backup:
|
||||||
|
|
||||||
|
1. List archives: `borgmatic list`
|
||||||
|
2. Extract dump from archive using `borg extract` to `/tmp/restore`
|
||||||
|
3. Restore with `pg_restore --clean --if-exists --no-owner --no-acl`
|
||||||
|
4. Fix ownership - ensure user `miniflux` owns all tables, sequences, and types in the `public` schema (restore runs as `eblume`)
|
||||||
|
5. Restart miniflux deployment
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ metadata:
|
||||||
name: miniflux-tailscale
|
name: miniflux-tailscale
|
||||||
namespace: miniflux
|
namespace: miniflux
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: tailscale
|
ingressClassName: tailscale
|
||||||
defaultBackend:
|
defaultBackend:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Manifests for the Tailscale Kubernetes Operator, managed via ArgoCD.
|
||||||
|
|
||||||
- `operator.yaml` - Static manifest from https://github.com/tailscale/tailscale/tree/main/cmd/k8s-operator/deploy/manifests
|
- `operator.yaml` - Static manifest from https://github.com/tailscale/tailscale/tree/main/cmd/k8s-operator/deploy/manifests
|
||||||
- Secret block removed from `operator.yaml` - managed separately via `secret.yaml.tpl`
|
- Secret block removed from `operator.yaml` - managed separately via `secret.yaml.tpl`
|
||||||
- Image reference changed to fully-qualified `docker.io/tailscale/k8s-operator:stable` for CRI-O compatibility
|
- Image reference changed to fully-qualified `docker.io/tailscale/k8s-operator:stable`
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ kubectl logs -n tailscale -l app.kubernetes.io/name=operator
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `kustomization.yaml` | Kustomize configuration for all manifests |
|
| `kustomization.yaml` | Kustomize configuration for all manifests |
|
||||||
| `operator.yaml` | Operator deployment, CRDs, RBAC (secret removed) |
|
| `operator.yaml` | Operator deployment, CRDs, RBAC (secret removed) |
|
||||||
| `proxyclass.yaml` | ProxyClass with fully-qualified images for CRI-O |
|
| `proxyclass.yaml` | ProxyClass with fully-qualified images |
|
||||||
| `dnsconfig.yaml` | DNSConfig for cluster-to-tailnet name resolution |
|
| `dnsconfig.yaml` | DNSConfig for cluster-to-tailnet name resolution |
|
||||||
| `egress-forge.yaml` | Egress proxy for accessing forge on indri |
|
| `egress-forge.yaml` | Egress proxy for accessing forge on indri |
|
||||||
| `secret.yaml.tpl` | 1Password template for OAuth credentials (manual) |
|
| `secret.yaml.tpl` | 1Password template for OAuth credentials (manual) |
|
||||||
|
|
@ -81,10 +81,10 @@ kubectl logs -n tailscale -l app.kubernetes.io/name=operator
|
||||||
|
|
||||||
- **TODO:** The OAuth secret (`operator-oauth`) is not managed by ArgoCD and must be applied
|
- **TODO:** The OAuth secret (`operator-oauth`) is not managed by ArgoCD and must be applied
|
||||||
manually. Future improvement: integrate with a secrets operator (e.g., External Secrets).
|
manually. Future improvement: integrate with a secrets operator (e.g., External Secrets).
|
||||||
- Services using the Tailscale LoadBalancer must reference the ProxyClass:
|
- Services using the Tailscale LoadBalancer should reference the ProxyClass:
|
||||||
```yaml
|
```yaml
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
```
|
```
|
||||||
- The egress proxy for forge targets `indri.tail8d86e.ts.net` directly (not `forge.tail8d86e.ts.net`)
|
- The egress proxy for forge targets `indri.tail8d86e.ts.net` directly (not `forge.tail8d86e.ts.net`)
|
||||||
because Tailscale Serve hostnames are virtual and only work via the Tailscale client.
|
because Tailscale Serve hostnames are virtual and only work via the Tailscale client.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ metadata:
|
||||||
namespace: tailscale
|
namespace: tailscale
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/tailnet-fqdn: indri.tail8d86e.ts.net
|
tailscale.com/tailnet-fqdn: indri.tail8d86e.ts.net
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "default"
|
||||||
spec:
|
spec:
|
||||||
type: ExternalName
|
type: ExternalName
|
||||||
externalName: placeholder
|
externalName: placeholder
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
# ProxyClass: crio-compat
|
# ProxyClass: default
|
||||||
#
|
#
|
||||||
# Why this exists:
|
# Specifies fully-qualified image names for Tailscale proxy pods.
|
||||||
# CRI-O (the container runtime used by minikube) cannot resolve short image
|
# This ensures consistent behavior across different container runtimes.
|
||||||
# names like "tailscale/tailscale:stable". It requires fully-qualified names
|
|
||||||
# with an explicit registry prefix (e.g., "docker.io/tailscale/tailscale:stable").
|
|
||||||
#
|
|
||||||
# The Tailscale operator creates proxy pods (StatefulSets) for each LoadBalancer
|
|
||||||
# Service or Ingress. By default, these pods use short image names which fail
|
|
||||||
# on CRI-O with "ImageInspectError".
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# Add this annotation to any Tailscale Service or Ingress:
|
# Add this annotation to any Tailscale Service or Ingress:
|
||||||
# tailscale.com/proxy-class: "crio-compat"
|
# tailscale.com/proxy-class: "default"
|
||||||
#
|
#
|
||||||
# This tells the operator to use the fully-qualified image names defined below
|
# This tells the operator to use the fully-qualified image names defined below
|
||||||
# when creating the proxy pod for that resource.
|
# when creating the proxy pod for that resource.
|
||||||
|
|
@ -19,7 +13,7 @@
|
||||||
apiVersion: tailscale.com/v1alpha1
|
apiVersion: tailscale.com/v1alpha1
|
||||||
kind: ProxyClass
|
kind: ProxyClass
|
||||||
metadata:
|
metadata:
|
||||||
name: crio-compat
|
name: default
|
||||||
spec:
|
spec:
|
||||||
statefulSet:
|
statefulSet:
|
||||||
pod:
|
pod:
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# kubectl exec credential plugin for 1Password
|
|
||||||
# Usage: kubectl-credential-1password <vault-id> <item-id> <cert-field> <key-field>
|
|
||||||
#
|
|
||||||
# Fetches client certificate and key from 1Password and outputs
|
|
||||||
# ExecCredential JSON for kubectl authentication.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
VAULT_ID="$1"
|
|
||||||
ITEM_ID="$2"
|
|
||||||
CERT_FIELD="$3"
|
|
||||||
KEY_FIELD="$4"
|
|
||||||
|
|
||||||
# Fetch credentials from 1Password (strips surrounding quotes from text fields)
|
|
||||||
CLIENT_CERT=$(op --vault "$VAULT_ID" item get "$ITEM_ID" --fields "$CERT_FIELD" | sed 's/^"//; s/"$//')
|
|
||||||
CLIENT_KEY=$(op --vault "$VAULT_ID" item get "$ITEM_ID" --fields "$KEY_FIELD" | sed 's/^"//; s/"$//')
|
|
||||||
|
|
||||||
# Output ExecCredential JSON
|
|
||||||
# Note: jq is used to properly escape the PEM data for JSON
|
|
||||||
jq -n \
|
|
||||||
--arg cert "$CLIENT_CERT" \
|
|
||||||
--arg key "$CLIENT_KEY" \
|
|
||||||
'{
|
|
||||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
|
||||||
"kind": "ExecCredential",
|
|
||||||
"status": {
|
|
||||||
"clientCertificateData": $cert,
|
|
||||||
"clientKeyData": $key
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
59
mise-tasks/ensure-minikube-indri-kubectl-config
Executable file
59
mise-tasks/ensure-minikube-indri-kubectl-config
Executable file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#MISE description="Ensure kubectl config for minikube-indri is set up on this workstation"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.kube/minikube-indri"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/config.yml"
|
||||||
|
|
||||||
|
echo "Ensuring minikube-indri kubectl config..."
|
||||||
|
|
||||||
|
# Create directory if needed
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# Fetch certificates from indri
|
||||||
|
echo "Fetching certificates from indri..."
|
||||||
|
CA_CERT=$(ssh indri 'cat ~/.minikube/ca.crt')
|
||||||
|
CLIENT_CERT=$(ssh indri 'cat ~/.minikube/profiles/minikube/client.crt')
|
||||||
|
CLIENT_KEY=$(ssh indri 'cat ~/.minikube/profiles/minikube/client.key')
|
||||||
|
|
||||||
|
# Write certificate files
|
||||||
|
echo "$CA_CERT" > "$CONFIG_DIR/ca.crt"
|
||||||
|
echo "$CLIENT_CERT" > "$CONFIG_DIR/client.crt"
|
||||||
|
echo "$CLIENT_KEY" > "$CONFIG_DIR/client.key"
|
||||||
|
chmod 600 "$CONFIG_DIR/client.key"
|
||||||
|
|
||||||
|
# Write kubeconfig
|
||||||
|
cat > "$CONFIG_FILE" << EOF
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: $CONFIG_DIR/ca.crt
|
||||||
|
server: https://k8s.tail8d86e.ts.net
|
||||||
|
name: minikube-indri
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: minikube-indri
|
||||||
|
user: minikube-indri
|
||||||
|
name: minikube-indri
|
||||||
|
current-context: minikube-indri
|
||||||
|
users:
|
||||||
|
- name: minikube-indri
|
||||||
|
user:
|
||||||
|
client-certificate: $CONFIG_DIR/client.crt
|
||||||
|
client-key: $CONFIG_DIR/client.key
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Config written to $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Warn if KUBECONFIG doesn't include this file
|
||||||
|
if [[ -z "${KUBECONFIG:-}" ]] || [[ ":$KUBECONFIG:" != *":$CONFIG_FILE:"* ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "WARNING: KUBECONFIG does not include $CONFIG_FILE"
|
||||||
|
echo "Add this to your shell config:"
|
||||||
|
echo " export KUBECONFIG=\"\$KUBECONFIG:$CONFIG_FILE\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test with: kubectl --context=minikube-indri get nodes"
|
||||||
208
plans/k8s-migration/P5.1_docker_migration.md
Normal file
208
plans/k8s-migration/P5.1_docker_migration.md
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
# Phase 5.1: Migrate Minikube from QEMU2 to Docker Driver
|
||||||
|
|
||||||
|
**Goal**: Replace the qemu2 driver with docker to fix remote API access and simplify volume mounts
|
||||||
|
|
||||||
|
**Status**: Complete (2026-01-21) - Cluster running, ArgoCD deployed, apps synced
|
||||||
|
|
||||||
|
**Prerequisites**: [Phase 5](P5_devpi.complete.md) complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
### Original Problem (Podman → QEMU2)
|
||||||
|
|
||||||
|
During Phase 6 (Kiwix/Transmission migration), we discovered that the **podman driver has fundamental limitations** that prevent mounting external volumes:
|
||||||
|
|
||||||
|
1. **SMB CSI driver fails** with "Operation not permitted" - the rootless container lacks kernel-level mount capabilities
|
||||||
|
2. **`minikube mount` fails** - 9p mount gets "permission denied" inside the podman VM
|
||||||
|
3. **hostPath volumes** only work for paths inside the minikube container, not the macOS host
|
||||||
|
|
||||||
|
We migrated to QEMU2 to get a full VM with kernel capabilities.
|
||||||
|
|
||||||
|
### New Problem (QEMU2 → Docker)
|
||||||
|
|
||||||
|
The QEMU2 driver introduced a **new problem**: the Kubernetes API server is inside the VM at `192.168.105.2:6443`, and Tailscale's TCP proxy cannot forward to it properly:
|
||||||
|
|
||||||
|
- TCP connections succeed (nc -zv works)
|
||||||
|
- TLS handshake times out
|
||||||
|
- Root cause unknown, but likely related to Tailscale serve's handling of non-localhost upstreams
|
||||||
|
|
||||||
|
Additionally, the volume mount solution with QEMU2 was complex:
|
||||||
|
- Required NFS mount from sifaka → indri
|
||||||
|
- Then `minikube mount` to pass through to VM
|
||||||
|
- Two LaunchAgents/LaunchDaemons for persistence
|
||||||
|
- macOS GUI approval required for network access
|
||||||
|
|
||||||
|
### Why Docker?
|
||||||
|
|
||||||
|
The **docker driver** solves both problems:
|
||||||
|
|
||||||
|
1. **API Server on localhost**: Docker Desktop handles port forwarding from container to localhost automatically, so `tailscale serve --tcp=443 tcp://localhost:PORT` works
|
||||||
|
|
||||||
|
2. **Simpler volume mounts**: Docker Desktop has built-in macOS file sharing. Paths shared with Docker are accessible inside containers.
|
||||||
|
|
||||||
|
3. **Official Tailscale recommendation**: Tailscale's own [Kubernetes guide](https://tailscale.com/learn/managing-access-to-kubernetes-with-tailscale) uses minikube with the docker driver.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
|
||||||
|
### Infrastructure Changes
|
||||||
|
|
||||||
|
1. **Docker Desktop installed** (manual via `brew install --cask docker`)
|
||||||
|
- Configured with 12GB memory in Docker Desktop settings
|
||||||
|
- Kubernetes option disabled (using minikube instead)
|
||||||
|
|
||||||
|
2. **Docker minikube cluster created**:
|
||||||
|
```bash
|
||||||
|
minikube start \
|
||||||
|
--driver=docker \
|
||||||
|
--container-runtime=docker \
|
||||||
|
--cpus=6 \
|
||||||
|
--memory=11264 \
|
||||||
|
--disk-size=200g \
|
||||||
|
--apiserver-names=k8s.tail8d86e.ts.net,indri \
|
||||||
|
--apiserver-port=6443 \
|
||||||
|
--listen-address=0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Tailscale serve configured** for k8s API:
|
||||||
|
- API server on localhost (port is dynamic with docker driver)
|
||||||
|
- `tailscale serve --service=svc:k8s --tcp=443 tcp://localhost:<PORT>`
|
||||||
|
|
||||||
|
4. **Remote kubectl access working** from gilbert:
|
||||||
|
- Created `mise-tasks/ensure-minikube-indri-kubectl-config` script
|
||||||
|
- Fetches certs from indri and sets up `~/.kube/minikube-indri/config.yml`
|
||||||
|
|
||||||
|
### Ansible Roles Updated
|
||||||
|
|
||||||
|
- `ansible/roles/minikube/` - docker driver, removed qemu2/NFS/socket_vmnet
|
||||||
|
- `ansible/roles/tailscale_serve/` - removed svc:k8s (minikube role handles dynamic port)
|
||||||
|
- Containerd registry mirrors configured for zot pull-through cache
|
||||||
|
|
||||||
|
### ArgoCD Bootstrap
|
||||||
|
|
||||||
|
All apps deployed and synced from `feature/p5.1-qemu2-migration` branch:
|
||||||
|
|
||||||
|
| App | Status | Notes |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| tailscale-operator | Healthy | Manages Tailscale ingresses |
|
||||||
|
| argocd | Healthy | Self-managed |
|
||||||
|
| cloudnative-pg | Healthy | PostgreSQL operator |
|
||||||
|
| blumeops-pg | Progressing | PostgreSQL cluster starting |
|
||||||
|
| grafana | Progressing | Needs grafana-admin secret |
|
||||||
|
| grafana-config | Healthy | Dashboards and ingress |
|
||||||
|
| miniflux | Progressing | Needs miniflux-config secret |
|
||||||
|
| devpi | Progressing | Starting up |
|
||||||
|
|
||||||
|
### Secrets Still Needed
|
||||||
|
|
||||||
|
After PR merge, apply these secrets manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Grafana admin password
|
||||||
|
op inject -i argocd/manifests/grafana-config/secret-admin.yaml.tpl | kubectl --context=minikube-indri apply -f -
|
||||||
|
|
||||||
|
# Miniflux config
|
||||||
|
op inject -i argocd/manifests/miniflux/secret.yaml.tpl | kubectl --context=minikube-indri apply -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
### API Server Port
|
||||||
|
|
||||||
|
With docker driver, the API server port is **dynamic** - Docker maps a random host port to 6443 inside the container.
|
||||||
|
|
||||||
|
The minikube ansible role queries the port after cluster start and configures tailscale serve accordingly.
|
||||||
|
|
||||||
|
### Registry Mirror Configuration
|
||||||
|
|
||||||
|
Containerd uses `/etc/containerd/certs.d/<registry>/hosts.toml` files. The ansible role configures mirrors for:
|
||||||
|
- `registry.tail8d86e.ts.net` (private images)
|
||||||
|
- `docker.io`
|
||||||
|
- `ghcr.io`
|
||||||
|
- `quay.io`
|
||||||
|
|
||||||
|
### ProxyClass Renamed
|
||||||
|
|
||||||
|
Changed from `crio-compat` to `default` - the old name was misleading since we're no longer using CRI-O.
|
||||||
|
|
||||||
|
### Volume Mounts for P6 (Kiwix/Transmission)
|
||||||
|
|
||||||
|
**Solution: Direct NFS from pods to sifaka** ✅ TESTED AND WORKING
|
||||||
|
|
||||||
|
Docker NATs outbound traffic through indri's LAN IP (192.168.1.50), so sifaka's NFS exports need to allow `192.168.1.0/24`.
|
||||||
|
|
||||||
|
Sifaka NFS exports configured:
|
||||||
|
- `192.168.1.0/24` - Docker containers via indri NAT
|
||||||
|
- `100.64.0.0/10` - Tailscale clients
|
||||||
|
|
||||||
|
Pods can mount NFS directly:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- name: torrents
|
||||||
|
nfs:
|
||||||
|
server: sifaka
|
||||||
|
path: /volume1/torrents
|
||||||
|
```
|
||||||
|
|
||||||
|
No LaunchAgents, no `minikube mount`, no SMB CSI driver needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [x] Docker Desktop installed and running on indri
|
||||||
|
- [x] QEMU2 minikube deleted
|
||||||
|
- [x] Docker minikube running (6 CPUs, 11GB RAM)
|
||||||
|
- [x] API server accessible on localhost
|
||||||
|
- [x] Tailscale serve configured for svc:k8s
|
||||||
|
- [x] Remote kubectl access working from gilbert
|
||||||
|
- [x] Ansible roles updated for docker driver
|
||||||
|
- [x] socket_vmnet stopped
|
||||||
|
- [x] ArgoCD deployed and synced
|
||||||
|
- [x] All apps synced to feature branch
|
||||||
|
- [x] Apply app secrets (grafana-admin, miniflux-db, devpi-root, eblume, borgmatic)
|
||||||
|
- [x] Verify all apps healthy after secrets applied
|
||||||
|
- [x] Miniflux database restored from borgmatic backup
|
||||||
|
- [ ] Merge PR and reset apps to main branch
|
||||||
|
- [ ] `mise run indri-services-check` passes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Merge Steps
|
||||||
|
|
||||||
|
After PR is merged:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset all blumeops apps to main branch
|
||||||
|
argocd app set apps --revision main
|
||||||
|
argocd app set argocd --revision main
|
||||||
|
argocd app set blumeops-pg --revision main
|
||||||
|
argocd app set devpi --revision main
|
||||||
|
argocd app set grafana-config --revision main
|
||||||
|
argocd app set miniflux --revision main
|
||||||
|
argocd app set tailscale-operator --revision main
|
||||||
|
|
||||||
|
# Sync all apps
|
||||||
|
argocd app sync apps
|
||||||
|
argocd app sync argocd
|
||||||
|
argocd app sync tailscale-operator
|
||||||
|
argocd app sync blumeops-pg
|
||||||
|
argocd app sync grafana-config
|
||||||
|
argocd app sync miniflux
|
||||||
|
argocd app sync devpi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If Docker driver doesn't work:
|
||||||
|
|
||||||
|
1. Delete Docker minikube: `minikube delete`
|
||||||
|
2. Recreate QEMU2 cluster (restore old ansible config from git)
|
||||||
|
3. Accept the Tailscale TCP forwarding limitation and use SSH tunnel for remote kubectl
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
# Phase 5.1: Migrate Minikube from Podman to QEMU2 Driver
|
|
||||||
|
|
||||||
**Goal**: Replace the podman driver with qemu2 to enable proper volume mounts (hostPath, NFS, SMB CSI)
|
|
||||||
|
|
||||||
**Status**: Planning
|
|
||||||
|
|
||||||
**Prerequisites**: [Phase 5](P5_devpi.complete.md) complete
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
During Phase 6 (Kiwix/Transmission migration), we discovered that the **podman driver has fundamental limitations** that prevent mounting external volumes:
|
|
||||||
|
|
||||||
1. **SMB CSI driver fails** with "Operation not permitted" - the rootless container lacks kernel-level mount capabilities
|
|
||||||
2. **`minikube mount` fails** - 9p mount gets "permission denied" inside the podman VM
|
|
||||||
3. **hostPath volumes** only work for paths inside the minikube container, not the macOS host
|
|
||||||
|
|
||||||
These are documented limitations of the podman driver, which is labeled "experimental" in the [minikube documentation](https://minikube.sigs.k8s.io/docs/drivers/podman/).
|
|
||||||
|
|
||||||
### Failed P6 Attempt
|
|
||||||
|
|
||||||
Branch `feature/p6-kiwix-transmission` contains the P6 implementation that was blocked by these issues. The manifests are complete and tested, but couldn't mount the torrents volume.
|
|
||||||
|
|
||||||
**What was tried:**
|
|
||||||
- NFS volume mounts - failed due to missing CAP_SYS_ADMIN in podman container
|
|
||||||
- SMB CSI driver (v1.17.0) - mount fails with EPERM (same root cause)
|
|
||||||
- `minikube mount /Volumes/torrents:/Volumes/torrents` - 9p mount permission denied
|
|
||||||
- hostPath PV pointing to `/Volumes/torrents` - path doesn't exist inside minikube container
|
|
||||||
- Installing cifs-utils in minikube VM - still fails at kernel level
|
|
||||||
|
|
||||||
All of these failures trace back to the same root cause: the podman driver runs minikube in a rootless container that lacks the kernel capabilities required for filesystem mounts.
|
|
||||||
|
|
||||||
### Why QEMU2?
|
|
||||||
|
|
||||||
Multiple sources recommend QEMU2 as the best driver for Apple Silicon Macs:
|
|
||||||
|
|
||||||
> "Qemu emulator is the best option to run a Kubernetes Cluster using minikube on MAC arm64-based systems without any issues."
|
|
||||||
> — [DevOpsCube](https://devopscube.com/minikube-mac/)
|
|
||||||
|
|
||||||
QEMU2 creates an actual VM (not a container), which has:
|
|
||||||
- Full kernel capabilities for mounts
|
|
||||||
- Proper 9p/virtio filesystem support
|
|
||||||
- Native NFS client support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
### 1. Export Current State
|
|
||||||
|
|
||||||
Before destroying the cluster, capture the current state:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all ArgoCD apps and their sync status
|
|
||||||
argocd app list
|
|
||||||
|
|
||||||
# Backup any runtime state that matters (should be minimal - everything is in git)
|
|
||||||
kubectl --context=minikube-indri get all --all-namespaces -o yaml > /tmp/k8s-backup.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Stop and Delete Podman Minikube
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop the cluster
|
|
||||||
minikube stop
|
|
||||||
|
|
||||||
# Delete the cluster and all data
|
|
||||||
minikube delete
|
|
||||||
|
|
||||||
# Verify podman VM is cleaned up
|
|
||||||
podman machine list
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Update Ansible Roles for QEMU2
|
|
||||||
|
|
||||||
The installation must be orchestrated via ansible, following the existing patterns for `podman` and `minikube` roles.
|
|
||||||
|
|
||||||
**Changes needed:**
|
|
||||||
|
|
||||||
1. **Update `ansible/roles/minikube/` role:**
|
|
||||||
- Change driver from `podman` to `qemu2`
|
|
||||||
- Add QEMU as a dependency (via Brewfile or role)
|
|
||||||
- Optionally add socket_vmnet for full networking support
|
|
||||||
- Update any driver-specific configuration
|
|
||||||
|
|
||||||
2. **Update `Brewfile`:**
|
|
||||||
```ruby
|
|
||||||
brew "qemu"
|
|
||||||
# Optional: brew "socket_vmnet"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Update minikube start command in role:**
|
|
||||||
```bash
|
|
||||||
minikube start \
|
|
||||||
--driver=qemu2 \
|
|
||||||
--cpus=4 \
|
|
||||||
--memory=8192 \
|
|
||||||
--disk-size=50g \
|
|
||||||
--container-runtime=containerd \
|
|
||||||
--kubernetes-version=stable
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Remove or update podman role** (may still be useful for container builds)
|
|
||||||
|
|
||||||
### 4. Run Ansible to Create QEMU2 Cluster
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run the updated minikube role
|
|
||||||
mise run provision-indri -- --tags minikube
|
|
||||||
|
|
||||||
# Verify cluster is running
|
|
||||||
minikube status
|
|
||||||
kubectl get nodes
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Configure Host Path Access
|
|
||||||
|
|
||||||
With QEMU2, we need to either:
|
|
||||||
|
|
||||||
**Option A: Use `minikube mount` (9p)**
|
|
||||||
```bash
|
|
||||||
# Start persistent mount (run in background or via launchd)
|
|
||||||
minikube mount /Volumes/torrents:/Volumes/torrents &
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B: Use NFS export from macOS**
|
|
||||||
```bash
|
|
||||||
# Add NFS export on macOS
|
|
||||||
echo "/Volumes/torrents -alldirs -mapall=$(id -u):$(id -g) -network 192.168.0.0 -mask 255.255.0.0" | sudo tee -a /etc/exports
|
|
||||||
sudo nfsd restart
|
|
||||||
|
|
||||||
# In k8s, use NFS volume type directly
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Test Volume Mount with Test Pod
|
|
||||||
|
|
||||||
Create a test pod that mounts the torrents volume:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: volume-test
|
|
||||||
namespace: default
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: test
|
|
||||||
image: busybox
|
|
||||||
command: ["sh", "-c", "ls -la /data && sleep 3600"]
|
|
||||||
volumeMounts:
|
|
||||||
- name: torrents
|
|
||||||
mountPath: /data
|
|
||||||
volumes:
|
|
||||||
- name: torrents
|
|
||||||
hostPath:
|
|
||||||
path: /Volumes/torrents
|
|
||||||
type: Directory
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
```bash
|
|
||||||
kubectl apply -f volume-test.yaml
|
|
||||||
kubectl logs volume-test
|
|
||||||
kubectl exec volume-test -- ls -la /data
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Redeploy ArgoCD and Existing Apps
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Re-add ArgoCD
|
|
||||||
kubectl create namespace argocd
|
|
||||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
|
||||||
|
|
||||||
# Wait for ArgoCD to be ready
|
|
||||||
kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=300s
|
|
||||||
|
|
||||||
# Re-configure ArgoCD (repo credentials, etc.)
|
|
||||||
# ... follow P1 setup steps ...
|
|
||||||
|
|
||||||
# Sync all apps
|
|
||||||
argocd app sync apps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. Verify All Services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run health check
|
|
||||||
mise run indri-services-check
|
|
||||||
|
|
||||||
# Verify each k8s service
|
|
||||||
argocd app list
|
|
||||||
kubectl get pods --all-namespaces
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Clean Up Test Pod
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl delete pod volume-test
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
- [ ] Podman minikube deleted
|
|
||||||
- [ ] QEMU2 minikube running
|
|
||||||
- [ ] `minikube mount` or NFS working
|
|
||||||
- [ ] Test pod can read `/Volumes/torrents`
|
|
||||||
- [ ] ArgoCD redeployed and synced
|
|
||||||
- [ ] All existing apps healthy (grafana, miniflux, devpi, etc.)
|
|
||||||
- [ ] PostgreSQL cluster healthy
|
|
||||||
- [ ] Test pod deleted
|
|
||||||
- [ ] `mise run indri-services-check` passes (except intentionally offline services)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If QEMU2 doesn't work:
|
|
||||||
|
|
||||||
1. Delete QEMU2 cluster: `minikube delete`
|
|
||||||
2. Recreate podman cluster following P0/P1 steps
|
|
||||||
3. Redeploy apps from git
|
|
||||||
|
|
||||||
All state is in git, so cluster recreation is straightforward.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The QEMU2 VM will use more resources than podman (actual VM vs container)
|
|
||||||
- First boot may be slower due to VM initialization
|
|
||||||
- socket_vmnet provides better networking but requires sudo setup
|
|
||||||
- Consider creating a LaunchAgent for `minikube mount` if using that approach
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
**Goal**: Migrate kiwix-serve and transmission torrent daemon to k8s with shared storage
|
**Goal**: Migrate kiwix-serve and transmission torrent daemon to k8s with shared storage
|
||||||
|
|
||||||
**Status**: BLOCKED - waiting for [Phase 5.1](P5.1_qemu2_migration.md) (QEMU2 migration)
|
**Status**: Ready to implement
|
||||||
|
|
||||||
**Prerequisites**: [Phase 5.1](P5.1_qemu2_migration.md) complete (minikube on QEMU2 driver)
|
**Prerequisites**: [Phase 5.1](P5.1_docker_migration.md) complete (minikube on docker driver)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -62,19 +62,18 @@ New architecture in k8s:
|
||||||
|
|
||||||
## Architecture Decisions
|
## Architecture Decisions
|
||||||
|
|
||||||
### Storage: SMB on Sifaka (or NFS after QEMU2 migration)
|
### Storage: Direct NFS to Sifaka ✅ TESTED
|
||||||
|
|
||||||
**Note:** The original plan chose SMB over NFS, but both failed with podman driver. After QEMU2 migration, either should work. SMB is still preferred for:
|
**Solution:** Direct NFS volume mounts from pods to sifaka. No SMB CSI driver or `minikube mount` needed.
|
||||||
- Native Synology SMB support with good macOS compatibility
|
|
||||||
- ReadWriteMany access mode for concurrent pod access
|
|
||||||
- SMB CSI driver already mirrored to forge
|
|
||||||
|
|
||||||
**Alternative after QEMU2:** NFS may be simpler with `minikube mount` or direct NFS volume type.
|
With the docker driver, minikube containers NAT outbound traffic through indri's LAN IP (192.168.1.50). Sifaka's NFS exports are configured to allow:
|
||||||
|
- `192.168.1.0/24` - Docker containers via indri NAT
|
||||||
|
- `100.64.0.0/10` - Tailscale clients
|
||||||
|
|
||||||
**Storage path:** `/volume1/torrents/` on sifaka (SMB share name: `torrents`)
|
**Storage path:** `/volume1/torrents/` on sifaka (NFS export)
|
||||||
- General-purpose torrent download directory
|
- General-purpose torrent download directory
|
||||||
- Contains ZIM files, Linux ISOs, and whatever else users download
|
- Contains ZIM files, Linux ISOs, and whatever else users download
|
||||||
- Accessed via SMB credentials stored in k8s Secret
|
- Accessed via native k8s NFS volume (no credentials needed - IP-based access)
|
||||||
|
|
||||||
**No backup needed:**
|
**No backup needed:**
|
||||||
- Sifaka is RAID 5/6, already the backup target
|
- Sifaka is RAID 5/6, already the backup target
|
||||||
|
|
@ -142,49 +141,19 @@ This allows adding new ZIM archives by:
|
||||||
|
|
||||||
## Prerequisites (Manual Steps)
|
## Prerequisites (Manual Steps)
|
||||||
|
|
||||||
### 1. Configure SMB Share on Sifaka
|
### 1. Configure NFS Export on Sifaka
|
||||||
|
|
||||||
**Status: DONE** - The `torrents` shared folder has been created at `/volume1/torrents`.
|
**Status: DONE** - The `torrents` shared folder exists at `/volume1/torrents` with NFS exports allowing:
|
||||||
|
- `192.168.1.0/24` - Docker containers via indri NAT
|
||||||
|
- `100.64.0.0/10` - Tailscale clients
|
||||||
|
|
||||||
### 2. Create Dedicated Synology User for Kubernetes (USER ACTION REQUIRED)
|
### 2. Copy Existing Downloads to Sifaka
|
||||||
|
|
||||||
Create a dedicated Synology user for k8s SMB access (do not use personal account):
|
|
||||||
|
|
||||||
On Synology DSM (Control Panel → User & Group):
|
|
||||||
1. Create new user: `k8s-smb` (or similar)
|
|
||||||
- Set a strong password
|
|
||||||
- No admin privileges needed
|
|
||||||
- Deny access to all applications (only needs file services)
|
|
||||||
2. Set permissions on the `torrents` share:
|
|
||||||
- Give `k8s-smb` user Read/Write access
|
|
||||||
- Remove or limit other user access as appropriate
|
|
||||||
3. Store credentials in 1Password:
|
|
||||||
- Vault: `vg6xf6vvfmoh5hqjjhlhbeoaie` (blumeops vault)
|
|
||||||
- Item name: `synology-smb-k8s`
|
|
||||||
- Fields: `username` (k8s-smb), `password`
|
|
||||||
|
|
||||||
### 3. Mirror SMB CSI Driver Helm Chart to Forge (USER ACTION REQUIRED)
|
|
||||||
|
|
||||||
Mirror the SMB CSI driver chart to forge for GitOps deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the upstream chart repo
|
|
||||||
cd ~/code/3rd
|
|
||||||
git clone https://github.com/kubernetes-csi/csi-driver-smb.git
|
|
||||||
cd csi-driver-smb
|
|
||||||
|
|
||||||
# Push to forge mirror
|
|
||||||
git remote add forge ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/csi-driver-smb.git
|
|
||||||
git push forge --all --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Copy Existing Downloads to Sifaka
|
|
||||||
|
|
||||||
Before migration, copy existing downloads to avoid re-downloading ~138GB:
|
Before migration, copy existing downloads to avoid re-downloading ~138GB:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From indri - mount the SMB share via Finder or command line
|
# From indri - mount the NFS share
|
||||||
open smb://sifaka/torrents
|
sudo mount -t nfs sifaka:/volume1/torrents /Volumes/torrents
|
||||||
|
|
||||||
# Then rsync (adjust mount path as needed)
|
# Then rsync (adjust mount path as needed)
|
||||||
rsync -avP ~/transmission/ /Volumes/torrents/
|
rsync -avP ~/transmission/ /Volumes/torrents/
|
||||||
|
|
@ -193,69 +162,21 @@ rsync -avP ~/transmission/ /Volumes/torrents/
|
||||||
ls -la /Volumes/torrents/*.zim
|
ls -la /Volumes/torrents/*.zim
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Store SMB Credentials in 1Password
|
|
||||||
|
|
||||||
**Note:** This is covered in step 2 above. The 1Password item should be:
|
|
||||||
- Vault: `vg6xf6vvfmoh5hqjjhlhbeoaie` (blumeops vault)
|
|
||||||
- Item name: `synology-smb-k8s`
|
|
||||||
- Fields: `username` (k8s-smb), `password`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
### 1. Deploy SMB CSI Driver via ArgoCD
|
### 1. Create Shared NFS PersistentVolume
|
||||||
|
|
||||||
**File:** `argocd/manifests/smb-csi/values.yaml`
|
This PV is shared between transmission and kiwix namespaces. Uses direct NFS - no CSI driver needed.
|
||||||
|
|
||||||
```yaml
|
**File:** `argocd/manifests/torrent/pv-nfs.yaml`
|
||||||
# Minimal values - defaults are generally fine
|
|
||||||
controller:
|
|
||||||
replicas: 1
|
|
||||||
```
|
|
||||||
|
|
||||||
**File:** `argocd/apps/smb-csi.yaml`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: smb-csi
|
|
||||||
namespace: argocd
|
|
||||||
spec:
|
|
||||||
project: default
|
|
||||||
sources:
|
|
||||||
# Helm chart from forge mirror
|
|
||||||
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/csi-driver-smb.git
|
|
||||||
targetRevision: v1.17.0
|
|
||||||
path: charts/csi-driver-smb
|
|
||||||
helm:
|
|
||||||
releaseName: csi-driver-smb
|
|
||||||
valueFiles:
|
|
||||||
- $values/argocd/manifests/smb-csi/values.yaml
|
|
||||||
# Values from our git repo
|
|
||||||
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
|
|
||||||
targetRevision: main
|
|
||||||
ref: values
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: kube-system
|
|
||||||
syncPolicy:
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create Shared SMB PersistentVolume
|
|
||||||
|
|
||||||
This PV is shared between transmission and kiwix namespaces.
|
|
||||||
|
|
||||||
**File:** `argocd/manifests/torrent/pv-smb.yaml`
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolume
|
kind: PersistentVolume
|
||||||
metadata:
|
metadata:
|
||||||
name: torrents-smb-pv
|
name: torrents-nfs-pv
|
||||||
spec:
|
spec:
|
||||||
capacity:
|
capacity:
|
||||||
storage: 1Ti
|
storage: 1Ti
|
||||||
|
|
@ -263,43 +184,12 @@ spec:
|
||||||
- ReadWriteMany
|
- ReadWriteMany
|
||||||
persistentVolumeReclaimPolicy: Retain
|
persistentVolumeReclaimPolicy: Retain
|
||||||
storageClassName: ""
|
storageClassName: ""
|
||||||
mountOptions:
|
nfs:
|
||||||
- dir_mode=0777
|
server: sifaka
|
||||||
- file_mode=0777
|
path: /volume1/torrents
|
||||||
- uid=1000
|
|
||||||
- gid=1000
|
|
||||||
- noperm
|
|
||||||
- mfsymlinks
|
|
||||||
- cache=strict
|
|
||||||
- noserverino # Required to prevent data corruption
|
|
||||||
csi:
|
|
||||||
driver: smb.csi.k8s.io
|
|
||||||
volumeHandle: torrents-smb-pv
|
|
||||||
volumeAttributes:
|
|
||||||
source: //sifaka/torrents
|
|
||||||
nodeStageSecretRef:
|
|
||||||
name: smbcreds
|
|
||||||
namespace: torrent
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**File:** `argocd/manifests/torrent/secret-smb.yaml.tpl`
|
No secrets needed - NFS uses IP-based access control configured on sifaka.
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Template - apply manually with credentials from 1Password
|
|
||||||
# kubectl --context=minikube create secret generic smbcreds \
|
|
||||||
# --namespace torrent \
|
|
||||||
# --from-literal=username=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/username") \
|
|
||||||
# --from-literal=password=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/password")
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: smbcreds
|
|
||||||
namespace: torrent
|
|
||||||
type: Opaque
|
|
||||||
stringData:
|
|
||||||
username: "{{ op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/username }}"
|
|
||||||
password: "{{ op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/password }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -319,7 +209,7 @@ spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteMany
|
- ReadWriteMany
|
||||||
storageClassName: ""
|
storageClassName: ""
|
||||||
volumeName: torrents-smb-pv
|
volumeName: torrents-nfs-pv
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 1Ti
|
storage: 1Ti
|
||||||
|
|
@ -439,8 +329,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
namespace: torrent
|
namespace: torrent
|
||||||
resources:
|
resources:
|
||||||
- pv-smb.yaml
|
- pv-nfs.yaml
|
||||||
- secret-smb.yaml.tpl
|
|
||||||
- pvc.yaml
|
- pvc.yaml
|
||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
|
|
@ -473,7 +362,7 @@ spec:
|
||||||
|
|
||||||
## Kiwix Service
|
## Kiwix Service
|
||||||
|
|
||||||
### 3. Create Kiwix PVC (References Same PV)
|
### 2. Create Kiwix PVC (References Same PV)
|
||||||
|
|
||||||
**File:** `argocd/manifests/kiwix/pvc.yaml`
|
**File:** `argocd/manifests/kiwix/pvc.yaml`
|
||||||
|
|
||||||
|
|
@ -487,7 +376,7 @@ spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteMany # Need write for the sync sidecar to work
|
- ReadWriteMany # Need write for the sync sidecar to work
|
||||||
storageClassName: ""
|
storageClassName: ""
|
||||||
volumeName: torrents-smb-pv
|
volumeName: torrents-nfs-pv
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 1Ti
|
storage: 1Ti
|
||||||
|
|
@ -1096,10 +985,7 @@ If migration fails:
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| **Transmission (torrent namespace)** | |
|
| **Transmission (torrent namespace)** | |
|
||||||
| `argocd/apps/torrent.yaml` | ArgoCD Application for transmission |
|
| `argocd/apps/torrent.yaml` | ArgoCD Application for transmission |
|
||||||
| `argocd/apps/smb-csi.yaml` | ArgoCD Application for SMB CSI driver |
|
| `argocd/manifests/torrent/pv-nfs.yaml` | Shared NFS PersistentVolume |
|
||||||
| `argocd/manifests/smb-csi/values.yaml` | SMB CSI driver Helm values |
|
|
||||||
| `argocd/manifests/torrent/pv-smb.yaml` | Shared SMB PersistentVolume |
|
|
||||||
| `argocd/manifests/torrent/secret-smb.yaml.tpl` | SMB credentials secret template |
|
|
||||||
| `argocd/manifests/torrent/pvc.yaml` | Transmission PVC |
|
| `argocd/manifests/torrent/pvc.yaml` | Transmission PVC |
|
||||||
| `argocd/manifests/torrent/deployment.yaml` | Transmission deployment |
|
| `argocd/manifests/torrent/deployment.yaml` | Transmission deployment |
|
||||||
| `argocd/manifests/torrent/service.yaml` | Transmission service |
|
| `argocd/manifests/torrent/service.yaml` | Transmission service |
|
||||||
|
|
@ -1134,11 +1020,10 @@ If migration fails:
|
||||||
|
|
||||||
## Verification Checklist
|
## Verification Checklist
|
||||||
|
|
||||||
- [x] SMB share configured on sifaka (`/volume1/torrents`)
|
- [x] NFS export configured on sifaka (`/volume1/torrents`)
|
||||||
- [ ] Dedicated Synology user (`k8s-smb`) created for k8s access
|
- [x] NFS exports allow 192.168.1.0/24 and 100.64.0.0/10
|
||||||
- [ ] SMB CSI driver deployed to k8s
|
- [x] Direct NFS mount from pod tested and working
|
||||||
- [ ] Existing downloads copied to sifaka
|
- [ ] Existing downloads copied to sifaka
|
||||||
- [ ] SMB credentials secret created in k8s (using `k8s-smb` user)
|
|
||||||
- [ ] Transmission pod running in k8s (`torrent` namespace)
|
- [ ] Transmission pod running in k8s (`torrent` namespace)
|
||||||
- [ ] https://torrent.tail8d86e.ts.net accessible (web UI)
|
- [ ] https://torrent.tail8d86e.ts.net accessible (web UI)
|
||||||
- [ ] Can add torrents manually via web UI
|
- [ ] Can add torrents manually via web UI
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue