# MinIO Binary Distribution Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace PVC-based binary distribution with MinIO S3 fetch, eliminating the L40S autoscale-for-PVC-writes problem.

**Architecture:** Deploy job uploads compiled binaries to MinIO via `aws s3 cp`. Service pods and training jobs fetch their binary at startup via rclone initContainer into an emptyDir. Two binary PVCs are deleted.

**Tech Stack:** MinIO (in-cluster S3), rclone (in runtime images), aws-cli (in infra-runner)

---

### Task 1: Replace binary-writer pods with MinIO upload in deploy job

**Files:**
- Modify: `.gitlab-ci.yml:1564-1681` (deploy job binary section)

**Step 1: Delete PVC apply lines and entire binary-writer block**

In `.gitlab-ci.yml`, find and delete these two PVC apply lines (around line 1565-1566):
```yaml
    - kubectl apply -f infra/k8s/storage/foxhunt-binaries-pvc.yaml
    - kubectl apply -f infra/k8s/training/training-binaries-pvc.yaml
```

Then delete the entire block from `# ── Step 1: Copy binaries to PVCs` through `echo "=== Binary deployment complete ==="` (lines ~1567-1681). This removes all binary-writer/training-binary-writer pod logic.

**Step 2: Add MinIO upload block**

Insert in place of the deleted block (same indentation level as other `- |` script blocks in the deploy job):

```yaml
    # ── Step 1: Upload binaries to MinIO (replaces PVC binary-writer pods) ──
    - |
      if [ -d "build-out/services" ] || [ -d "build-out/training" ] || [ -d "web-dashboard/dist" ]; then
        set -e
        echo "=== Uploading binaries to MinIO ==="
        export AWS_ACCESS_KEY_ID="${MINIO_ACCESS_KEY}"
        export AWS_SECRET_ACCESS_KEY="${MINIO_SECRET_KEY}"
        S3="aws s3 --endpoint-url https://minio.foxhunt.svc.cluster.local:9000 --no-verify-ssl"

        if [ -d "build-out/services" ]; then
          $S3 cp build-out/services/ s3://foxhunt-binaries/services/ --recursive
          echo "Service binaries uploaded to MinIO"
        fi
        if [ -d "build-out/training" ]; then
          $S3 cp build-out/training/ s3://foxhunt-binaries/training/ --recursive
          echo "Training binaries uploaded to MinIO"
        fi
        if [ -d "web-dashboard/dist" ]; then
          $S3 cp web-dashboard/dist/ s3://foxhunt-binaries/static/ --recursive
          echo "Web dashboard assets uploaded to MinIO"
        fi
        echo "=== MinIO upload complete ==="
      else
        echo "No new binaries — compile jobs did not run"
      fi
```

**Step 3: Verify YAML syntax**

Run: `python3 -c "import yaml; yaml.safe_load(open('.gitlab-ci.yml'))" && echo OK`
Expected: `OK`

**Step 4: Commit**

```bash
git add .gitlab-ci.yml
git commit -m "refactor(ci): replace PVC binary-writers with MinIO upload

Deploy job now uploads binaries to MinIO via aws-cli (already in
infra-runner image). Eliminates binary-writer and training-binary-writer
pods, removing the L40S autoscale trigger for PVC writes."
```

---

### Task 2: Update trading-service.yaml — PVC to initContainer+emptyDir

**Files:**
- Modify: `infra/k8s/services/trading-service.yaml`

**Step 1: Add initContainer block**

Insert after `nodeSelector:` / before `containers:` (between lines 39 and 40):

```yaml
      initContainers:
        - name: fetch-binary
          image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-runtime:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -e
              rclone copyto \
                ":s3:foxhunt-binaries/services/trading_service" \
                "/binaries/trading_service" \
                --s3-provider=Minio \
                --s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 \
                --s3-access-key-id="${MINIO_ACCESS_KEY}" \
                --s3-secret-access-key="${MINIO_SECRET_KEY}" \
                --s3-no-check-bucket \
                --no-check-certificate
              chmod +x /binaries/trading_service
              echo "Fetched trading_service ($(stat -c%s /binaries/trading_service) bytes)"
          env:
            - name: MINIO_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: access-key
            - name: MINIO_SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: secret-key
          volumeMounts:
            - name: binaries
              mountPath: /binaries
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 128Mi
```

**Step 2: Replace PVC volume with emptyDir**

Replace:
```yaml
        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true
```

With:
```yaml
        - name: binaries
          emptyDir:
            sizeLimit: 200Mi
```

**Step 3: Remove readOnly from main container's binaries volumeMount**

The main container `volumeMounts` has `readOnly: true` on the binaries mount. The initContainer needs write access. Change:
```yaml
            - name: binaries
              mountPath: /binaries
              readOnly: true
```
To:
```yaml
            - name: binaries
              mountPath: /binaries
              readOnly: true
```

Actually — the initContainer writes, the main container reads. emptyDir supports this: the initContainer writes, then terminates, then the main container mounts the same emptyDir read-only. Keep `readOnly: true` on the main container's mount. The initContainer's mount does NOT have `readOnly: true`.

**Step 4: Verify YAML**

Run: `python3 -c "import yaml; list(yaml.safe_load_all(open('infra/k8s/services/trading-service.yaml')))" && echo OK`
Expected: `OK`

---

### Task 3: Update api-gateway.yaml

**Files:**
- Modify: `infra/k8s/services/api-gateway.yaml`

Same pattern as Task 2. Binary name: `api_gateway`

**Step 1: Add initContainer** (between `nodeSelector:` and `containers:`, after line 40)

Same initContainer as Task 2 but with binary name `api_gateway`:
- `rclone copyto ":s3:foxhunt-binaries/services/api_gateway" "/binaries/api_gateway"`
- `chmod +x /binaries/api_gateway`
- `echo "Fetched api_gateway ..."`

**Step 2: Replace PVC volume with emptyDir** (lines 122-126)

Replace:
```yaml
        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true
```
With:
```yaml
        - name: binaries
          emptyDir:
            sizeLimit: 200Mi
```

---

### Task 4: Update broker-gateway.yaml

**Files:**
- Modify: `infra/k8s/services/broker-gateway.yaml`

Same pattern. Binary name: `broker_gateway_service`

**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)

Binary: `broker_gateway_service`

**Step 2: Replace PVC volume with emptyDir** (lines 116-120)

---

### Task 5: Update ml-training-service.yaml

**Files:**
- Modify: `infra/k8s/services/ml-training-service.yaml`

Binary name: `ml_training_service`. This service already has `minio-credentials` env vars and `minio-ca` volume.

**Step 1: Add initContainer** (between `nodeSelector:` line 82 and `containers:` line 83)

Same pattern. Binary: `ml_training_service`. Command includes `"serve"` arg — that's on the main container, not the initContainer.

**Step 2: Replace PVC volume with emptyDir** (lines 166-170)

Replace:
```yaml
        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true
```
With:
```yaml
        - name: binaries
          emptyDir:
            sizeLimit: 200Mi
```

---

### Task 6: Update backtesting-service.yaml

**Files:**
- Modify: `infra/k8s/services/backtesting-service.yaml`

Binary name: `backtesting_service`

**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 105-109)

---

### Task 7: Update trading-agent-service.yaml

**Files:**
- Modify: `infra/k8s/services/trading-agent-service.yaml`

Binary name: `trading_agent_service`

**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 103-107)

---

### Task 8: Update data-acquisition-service.yaml

**Files:**
- Modify: `infra/k8s/services/data-acquisition-service.yaml`

Binary name: `data_acquisition_service`

**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 96-100)

---

### Task 9: Update web-gateway.yaml (special — needs static assets too)

**Files:**
- Modify: `infra/k8s/services/web-gateway.yaml`

Binary name: `web-gateway`. Additionally fetches static dashboard assets.

**Step 1: Add initContainer with dual fetch**

Insert between `nodeSelector:` line 39 and `containers:` line 40:

```yaml
      initContainers:
        - name: fetch-binary
          image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-runtime:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -e
              RCLONE_FLAGS="--s3-provider=Minio --s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 --s3-access-key-id=${MINIO_ACCESS_KEY} --s3-secret-access-key=${MINIO_SECRET_KEY} --s3-no-check-bucket --no-check-certificate"
              rclone copyto ":s3:foxhunt-binaries/services/web-gateway" "/binaries/web-gateway" $RCLONE_FLAGS
              chmod +x /binaries/web-gateway
              echo "Fetched web-gateway ($(stat -c%s /binaries/web-gateway) bytes)"
              rclone copy ":s3:foxhunt-binaries/static/" "/binaries/static/" $RCLONE_FLAGS
              echo "Fetched static assets"
          env:
            - name: MINIO_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: access-key
            - name: MINIO_SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: secret-key
          volumeMounts:
            - name: binaries
              mountPath: /binaries
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 256Mi
```

Note: `sizeLimit` on the emptyDir should be `500Mi` for web-gateway (binary + static assets).

**Step 2: Replace PVC volume with emptyDir** (lines 102-106)

Replace:
```yaml
        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true
```
With:
```yaml
        - name: binaries
          emptyDir:
            sizeLimit: 500Mi
```

---

### Task 10: Commit all service yaml changes

```bash
git add infra/k8s/services/
git commit -m "refactor(k8s): replace PVC mounts with MinIO rclone initContainers

All 8 service deployments now fetch their binary from MinIO at pod
startup via rclone initContainer into emptyDir. Eliminates dependency
on foxhunt-binaries PVC (RWO node affinity constraint).

web-gateway additionally fetches static dashboard assets."
```

---

### Task 11: Update training job-template.yaml

**Files:**
- Modify: `infra/k8s/training/job-template.yaml`

**Step 1: Replace existing fetch-binary initContainer**

The current initContainer (lines 41-73) copies from `training-binaries` PVC. Replace entire initContainer with:

```yaml
      initContainers:
        - name: fetch-binary
          image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-training-runtime:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -e
              rclone copyto \
                ":s3:foxhunt-binaries/training/$(TRAINING_BINARY)" \
                "/binaries/$(TRAINING_BINARY)" \
                --s3-provider=Minio \
                --s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 \
                --s3-access-key-id="${MINIO_ACCESS_KEY}" \
                --s3-secret-access-key="${MINIO_SECRET_KEY}" \
                --s3-no-check-bucket \
                --no-check-certificate
              chmod +x "/binaries/$(TRAINING_BINARY)"
              echo "Fetched $(TRAINING_BINARY) ($(stat -c%s /binaries/$(TRAINING_BINARY)) bytes)"
          env:
            - name: TRAINING_BINARY
              value: train_baseline_supervised
            - name: MINIO_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: access-key
            - name: MINIO_SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: secret-key
          volumeMounts:
            - name: binaries
              mountPath: /binaries
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 128Mi
```

**Step 2: Remove training-binaries-src volume**

Delete from volumes section (lines 122-127):
```yaml
        - name: training-binaries-src
          persistentVolumeClaim:
            claimName: training-binaries
            readOnly: true
```

**Step 3: Verify YAML**

Run: `python3 -c "import yaml; list(yaml.safe_load_all(open('infra/k8s/training/job-template.yaml')))" && echo OK`
Expected: `OK`

**Step 4: Commit**

```bash
git add infra/k8s/training/job-template.yaml
git commit -m "refactor(k8s): training job fetches binary from MinIO

Replace PVC-based training binary distribution with MinIO rclone
initContainer. Training jobs no longer need training-binaries PVC,
eliminating L40S GPU node autoscale just for binary distribution."
```

---

### Task 12: Delete PVC manifests

**Files:**
- Delete: `infra/k8s/storage/foxhunt-binaries-pvc.yaml`
- Delete: `infra/k8s/training/training-binaries-pvc.yaml`

**Step 1: Delete the files**

```bash
rm infra/k8s/storage/foxhunt-binaries-pvc.yaml
rm infra/k8s/training/training-binaries-pvc.yaml
```

**Step 2: Verify no remaining references to deleted PVCs**

Run: `grep -r "foxhunt-binaries" infra/ .gitlab-ci.yml --include='*.yaml' --include='*.yml'`
Expected: Only MinIO bucket references (`s3://foxhunt-binaries/`), no PVC references.

Run: `grep -r "training-binaries" infra/ .gitlab-ci.yml --include='*.yaml' --include='*.yml'`
Expected: No results (the PVC name is gone).

**Step 3: Commit**

```bash
git add -A infra/k8s/storage/foxhunt-binaries-pvc.yaml infra/k8s/training/training-binaries-pvc.yaml
git commit -m "chore(k8s): delete foxhunt-binaries and training-binaries PVCs

These PVCs are replaced by MinIO-based binary distribution.
Service pods and training jobs now fetch binaries from MinIO
via rclone initContainers.

NOTE: Run 'kubectl delete pvc foxhunt-binaries training-binaries -n foxhunt'
on the cluster to reclaim the block storage volumes."
```

---

### Task 13: Final verification

**Step 1: Grep for any remaining PVC binary references**

Run: `grep -rn "claimName: foxhunt-binaries\|claimName: training-binaries" infra/ .gitlab-ci.yml`
Expected: No results.

**Step 2: Grep for binary-writer references**

Run: `grep -rn "binary-writer\|binary_writer" .gitlab-ci.yml`
Expected: No results.

**Step 3: Verify all service yamls have initContainers**

Run: `grep -l "fetch-binary" infra/k8s/services/*.yaml | wc -l`
Expected: `8`

**Step 4: Verify training job has MinIO fetch**

Run: `grep "foxhunt-binaries/training" infra/k8s/training/job-template.yaml`
Expected: Match on the rclone line.

---

## Summary of Changes

| File | Action | Lines ~Δ |
|------|--------|----------|
| `.gitlab-ci.yml` | Replace binary-writer pods with `aws s3 cp` | -100/+20 |
| `infra/k8s/services/trading-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/api-gateway.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/broker-gateway.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/ml-training-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/backtesting-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/trading-agent-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/data-acquisition-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/web-gateway.yaml` | Add initContainer (binary+static), PVC→emptyDir | +35/-4 |
| `infra/k8s/training/job-template.yaml` | Replace PVC initContainer with MinIO | +30/-20 |
| `infra/k8s/storage/foxhunt-binaries-pvc.yaml` | Delete | -19 |
| `infra/k8s/training/training-binaries-pvc.yaml` | Delete | -19 |

**Total: 12 files, ~+275/-210 lines, 0 image rebuilds**

## Post-Deploy Cluster Cleanup

After deploying this change, manually delete the orphaned PVCs:
```bash
kubectl delete pvc foxhunt-binaries training-binaries -n foxhunt
```

Add `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY` as CI/CD variables in GitLab
(Settings → CI/CD → Variables), or source them from the cluster secret during the deploy job.
