diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..78f878a4d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,105 @@ +# Foxhunt devcontainer — extends CI builder with interactive dev tooling +# Built by CI, pushed to GitLab registry as devcontainer:latest +# See: docs/plans/2026-02-25-devcontainer-design.md + +ARG CI_BUILDER_IMAGE=rg.fr-par.scw.cloud/foxhunt-ci/ci-builder:latest +FROM ${CI_BUILDER_IMAGE} + +ENV DEBIAN_FRONTEND=noninteractive + +# Dev tooling: Node.js 22 (Claude Code), Python 3, shell tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-venv \ + zsh \ + sudo \ + openssh-server \ + git-lfs \ + jq \ + ripgrep \ + fd-find \ + htop \ + less \ + vim \ + && rm -rf /var/lib/apt/lists/* + +# Node.js 22 LTS (Claude Code runtime) +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Claude Code CLI +RUN npm install -g @anthropic-ai/claude-code + +# kubectl +RUN curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl + +# helm +RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# scw CLI (Scaleway) +RUN curl -fsSL https://github.com/scaleway/scaleway-cli/releases/latest/download/scaleway-cli_linux_amd64 \ + -o /usr/local/bin/scw \ + && chmod +x /usr/local/bin/scw + +# glab (GitLab CLI) +RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/permalink/latest/downloads/glab_linux_amd64.deb" \ + -o /tmp/glab.deb \ + && dpkg -i /tmp/glab.deb \ + && rm /tmp/glab.deb + +# terraform +RUN curl -fsSL https://releases.hashicorp.com/terraform/1.11.2/terraform_1.11.2_linux_amd64.zip \ + -o /tmp/terraform.zip \ + && unzip -o /tmp/terraform.zip -d /usr/local/bin \ + && rm /tmp/terraform.zip + +# terragrunt +RUN curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v0.72.6/terragrunt_linux_amd64 \ + -o /usr/local/bin/terragrunt \ + && chmod +x /usr/local/bin/terragrunt + +# yq (YAML processor) +RUN curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \ + -o /usr/local/bin/yq \ + && chmod +x /usr/local/bin/yq + +# Non-root dev user with sudo +RUN groupadd -g 1000 dev \ + && useradd -m -u 1000 -g dev -s /bin/zsh dev \ + && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev + +# Install oh-my-zsh for dev user +RUN su - dev -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended' + +# Move Rust toolchain to shared location so dev user can access it +# (CI builder installs to /root/.cargo and /root/.rustup) +RUN cp -a /root/.cargo /opt/cargo \ + && cp -a /root/.rustup /opt/rustup \ + && chown -R dev:dev /opt/cargo /opt/rustup + +# Environment for dev user +ENV CARGO_HOME=/home/dev/.cargo +ENV RUSTUP_HOME=/opt/rustup +ENV PATH="/home/dev/.cargo/bin:/opt/rustup/bin:/opt/cargo/bin:${PATH}" +ENV SQLX_OFFLINE=true +ENV CUDA_COMPUTE_CAP=90 + +# sccache local disk cache (PVC-backed home dir) +ENV SCCACHE_DIR=/home/dev/.cache/sccache +ENV RUSTC_WRAPPER=/usr/local/bin/sccache + +# First-run init script: copies cargo bins to home if not already present +COPY .devcontainer/init-home.sh /usr/local/bin/init-home.sh +RUN chmod +x /usr/local/bin/init-home.sh + +USER dev +WORKDIR /workspace + +# Verify key tools +RUN rustc --version && cargo --version && node --version && claude --version \ + && kubectl version --client 2>/dev/null && helm version --short \ + && terraform --version && terragrunt --version && scw version && glab --version diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d409dcc07 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "foxhunt", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "CI_BUILDER_IMAGE": "rg.fr-par.scw.cloud/foxhunt-ci/ci-builder:latest" + } + }, + "remoteUser": "dev", + "containerEnv": { + "SQLX_OFFLINE": "true", + "CUDA_COMPUTE_CAP": "90", + "EDITOR": "vim" + }, + "postCreateCommand": "/usr/local/bin/init-home.sh", + "postStartCommand": "claude --version && cargo --version && kubectl cluster-info 2>/dev/null || true", + "customizations": { + "devpod": { + "provider": "kubernetes" + } + } +} diff --git a/.devcontainer/init-home.sh b/.devcontainer/init-home.sh new file mode 100755 index 000000000..717da1122 --- /dev/null +++ b/.devcontainer/init-home.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +# Bootstrap cargo into PVC-backed home if not already present +if [ ! -d "$HOME/.cargo/bin" ]; then + echo "[init-home] First run: linking cargo toolchain..." + mkdir -p "$HOME/.cargo/bin" + # Copy cargo config and link binaries from shared rustup + for bin in /opt/cargo/bin/*; do + ln -sf "$bin" "$HOME/.cargo/bin/$(basename "$bin")" + done + echo "[init-home] Cargo toolchain linked." +fi + +# Ensure sccache cache dir exists +mkdir -p "${SCCACHE_DIR:-$HOME/.cache/sccache}" + +# Default .zshrc if none exists (oh-my-zsh is in the image, config in PVC) +if [ ! -f "$HOME/.zshrc" ]; then + cp /home/dev/.oh-my-zsh/templates/zshrc.zsh-template "$HOME/.zshrc" 2>/dev/null || true + echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> "$HOME/.zshrc" + echo 'export SQLX_OFFLINE=true' >> "$HOME/.zshrc" +fi + +echo "[init-home] Ready." diff --git a/.devcontainer/pod-template.yaml b/.devcontainer/pod-template.yaml new file mode 100644 index 000000000..737c72b90 --- /dev/null +++ b/.devcontainer/pod-template.yaml @@ -0,0 +1,41 @@ +# DevPod pod template — merged with DevPod's generated pod spec +# Ref: https://github.com/loft-sh/devpod-provider-kubernetes +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: devpod + app.kubernetes.io/part-of: foxhunt + app.kubernetes.io/component: dev-environment +spec: + nodeSelector: + k8s.scaleway.com/pool-name: gpu-dev + containers: + - name: devpod + resources: + requests: + cpu: "4000m" + memory: "16Gi" + limits: + cpu: "14000m" + memory: "56Gi" + volumeMounts: + - name: dev-home + mountPath: /home/dev + - name: training-data + mountPath: /data/training + readOnly: true + initContainers: + - name: init-home + image: busybox:latest + command: ["sh", "-c", "chown -R 1000:1000 /home/dev"] + volumeMounts: + - name: dev-home + mountPath: /home/dev + volumes: + - name: dev-home + persistentVolumeClaim: + claimName: dev-home-pvc + - name: training-data + persistentVolumeClaim: + claimName: training-data-pvc diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7cd7c3fce..84e1bda32 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,6 +57,35 @@ build-ci-builder: --dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.ci-builder" --destination "${CI_BUILDER_IMAGE}" +# -------------------------------------------------------------------------- +# Stage 0b: Build devcontainer image → push to internal GitLab registry +# -------------------------------------------------------------------------- +build-devcontainer: + stage: prepare + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + tags: + - kapsule + - docker + rules: + - if: $CI_PIPELINE_SOURCE == "push" + changes: + - .devcontainer/** + when: on_success + - when: manual + allow_failure: true + before_script: + - mkdir -p /kaniko/.docker + - | + echo "{\"auths\":{\"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}}}" > /kaniko/.docker/config.json + script: + - /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/.devcontainer/Dockerfile" + --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:${CI_COMMIT_SHA}" + --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:latest" + # Base template for Rust jobs — pre-baked CI builder from Scaleway CR # Image pre-exists in SCR; rebuild via build-ci-builder when Dockerfile changes .rust-base: diff --git a/infra/k8s/storage/dev-home-pvc.yaml b/infra/k8s/storage/dev-home-pvc.yaml new file mode 100644 index 000000000..35c8a6fac --- /dev/null +++ b/infra/k8s/storage/dev-home-pvc.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dev-home-pvc + namespace: foxhunt + labels: + app.kubernetes.io/name: dev-home + app.kubernetes.io/part-of: foxhunt + app.kubernetes.io/component: devpod +spec: + storageClassName: scw-bssd + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi diff --git a/infra/live/production/kapsule/terragrunt.hcl b/infra/live/production/kapsule/terragrunt.hcl index 7baa95a7b..758f02f51 100644 --- a/infra/live/production/kapsule/terragrunt.hcl +++ b/infra/live/production/kapsule/terragrunt.hcl @@ -26,4 +26,9 @@ inputs = { gitlab_type = "GP1-XS" # CI builds run on gpu-training pool (H100: 24 vCPU, 240GB, fast builds + GPU tests) + + # Dev pool (GP1-L for DevPod remote development, autoscales to zero) + enable_dev_pool = true + dev_pool_type = "GP1-L" + dev_pool_max_size = 1 } diff --git a/infra/modules/kapsule/main.tf b/infra/modules/kapsule/main.tf index 171dcdda7..aebf2cc03 100644 --- a/infra/modules/kapsule/main.tf +++ b/infra/modules/kapsule/main.tf @@ -89,3 +89,21 @@ resource "scaleway_k8s_pool" "gitlab" { region = var.region } +# CPU pool for remote development (DevPod — autoscales to zero) +resource "scaleway_k8s_pool" "dev" { + count = var.enable_dev_pool ? 1 : 0 + cluster_id = scaleway_k8s_cluster.foxhunt.id + name = "gpu-dev" + node_type = var.dev_pool_type + size = 1 + min_size = 0 + max_size = var.dev_pool_max_size + autoscaling = true + autohealing = true + region = var.region + + lifecycle { + ignore_changes = [size] + } +} + diff --git a/infra/modules/kapsule/variables.tf b/infra/modules/kapsule/variables.tf index c4fd9e2cb..a33f69890 100644 --- a/infra/modules/kapsule/variables.tf +++ b/infra/modules/kapsule/variables.tf @@ -69,3 +69,21 @@ variable "gitlab_type" { default = "DEV1-L" } +variable "enable_dev_pool" { + description = "Create CPU node pool for remote development (DevPod)" + type = bool + default = false +} + +variable "dev_pool_type" { + description = "Instance type for the dev node pool" + type = string + default = "GP1-L" +} + +variable "dev_pool_max_size" { + description = "Maximum number of nodes in the dev pool" + type = number + default = 1 +} + diff --git a/scripts/devpod-setup.sh b/scripts/devpod-setup.sh new file mode 100755 index 000000000..8d1dd3f65 --- /dev/null +++ b/scripts/devpod-setup.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -euo pipefail + +# DevPod one-time setup for Foxhunt development +# Prerequisites: devpod CLI installed, kubectl configured for foxhunt cluster +# +# Install DevPod: +# curl -fsSL https://devpod.sh/install.sh | bash +# +# Usage: +# ./scripts/devpod-setup.sh +# devpod up . # from the foxhunt repo root +# devpod up git@100.90.76.85:2222/root/foxhunt.git # from anywhere + +echo "=== Foxhunt DevPod Setup ===" + +# Check prerequisites +command -v devpod >/dev/null 2>&1 || { echo "Error: devpod not installed. Run: curl -fsSL https://devpod.sh/install.sh | bash"; exit 1; } +command -v kubectl >/dev/null 2>&1 || { echo "Error: kubectl not found"; exit 1; } + +# Verify cluster access +if ! kubectl -n foxhunt get ns foxhunt >/dev/null 2>&1; then + echo "Error: Cannot reach foxhunt namespace. Check kubectl config." + exit 1 +fi + +# Ensure PVC exists +if ! kubectl -n foxhunt get pvc dev-home-pvc >/dev/null 2>&1; then + echo "Creating dev-home-pvc (50Gi block storage)..." + kubectl apply -f infra/k8s/storage/dev-home-pvc.yaml +fi + +# Add kubernetes provider (idempotent) +devpod provider add kubernetes 2>/dev/null || true + +# Configure provider +devpod provider set kubernetes \ + KUBERNETES_NAMESPACE=foxhunt \ + NODE_SELECTOR="k8s.scaleway.com/pool-name=gpu-dev" \ + DISK_SIZE=10Gi \ + STORAGE_CLASS=scw-bssd \ + INACTIVITY_TIMEOUT=30m \ + IMAGE_PULL_SECRETS=gitlab-registry \ + POD_MANIFEST_TEMPLATE=.devcontainer/pod-template.yaml + +# Configure image pull secrets if not already present +if ! kubectl -n foxhunt get secret gitlab-registry >/dev/null 2>&1; then + echo "" + echo "NOTE: You need to create the gitlab-registry pull secret:" + echo " kubectl -n foxhunt create secret docker-registry gitlab-registry \\" + echo " --docker-server=gitlab-registry.foxhunt.svc.cluster.local:5000 \\" + echo " --docker-username= --docker-password=" + echo "" +fi + +echo "" +echo "=== Setup complete ===" +echo "" +echo "Usage:" +echo " devpod up . # from repo root" +echo " devpod up git@100.90.76.85:2222/root/foxhunt.git # from anywhere" +echo "" +echo "Open in DevPod link (bookmark this):" +echo " https://devpod.sh/open#ssh://git@100.90.76.85:2222/root/foxhunt.git" +echo "" +echo "Session lifecycle:" +echo " devpod up foxhunt # start/reconnect" +echo " devpod stop foxhunt # stop (PVC preserved, node scales down)" +echo " devpod delete foxhunt # delete workspace (PVC preserved)" +echo ""