commit 05ea40562c16efae18b01cf8ca19993f879d624b Author: Ubuntu Date: Sun Sep 20 12:57:44 2026 +0000 Initial cursor-devbox: image, deploy manifests, and Flux SHA automation. Owns Dockerfile/CI and node1 workload YAML; ImagePolicy elects run_number-sha tags and ImageUpdateAutomation commits them into deploy/. diff --git a/.gitea/workflows/build-and-push.yaml b/.gitea/workflows/build-and-push.yaml new file mode 100644 index 0000000..b836440 --- /dev/null +++ b/.gitea/workflows/build-and-push.yaml @@ -0,0 +1,33 @@ +name: build-and-push + +on: + push: + branches: [master] + paths: + - 'Dockerfile' + - 'entrypoint.sh' + - '.gitea/workflows/build-and-push.yaml' + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: dcr.trinitysan.by + username: ${{ secrets.DCR_USERNAME }} + password: ${{ secrets.DCR_PASSWORD }} + - uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: | + dcr.trinitysan.by/infra/cursor-devbox:latest + dcr.trinitysan.by/infra/cursor-devbox:${{ github.sha }} + dcr.trinitysan.by/infra/cursor-devbox:${{ github.run_number }}-${{ github.sha }} + cache-from: type=registry,ref=dcr.trinitysan.by/infra/cursor-devbox:build-cache + cache-to: type=registry,ref=dcr.trinitysan.by/infra/cursor-devbox:build-cache,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..04adfb1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# Cursor Remote-SSH / personal devbox: OpenSSH + docker CLI (DinD sidecar at runtime). +FROM ubuntu:latest + +ARG KUBECTL_VERSION=v1.31.2 +ARG FLUX_VERSION=2.9.4 + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + chromium \ + curl \ + docker.io \ + git \ + golang \ + jq \ + nodejs \ + npm \ + openssh-server \ + python3 \ + python3-venv \ + ripgrep \ + rustc \ + sudo \ + unzip \ + vim-tiny \ + wget \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /var/run/sshd /etc/ssh/sshd_config.d /usr/share/devbox-http \ + && echo 'devbox ok' > /usr/share/devbox-http/index.html + +# kubectl (same pattern as services/devops-ai) +RUN curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 \ + "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \ + -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl + +# flux CLI +RUN curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 \ + "https://github.com/fluxcd/flux2/releases/download/v${FLUX_VERSION}/flux_${FLUX_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C /usr/local/bin flux \ + && chmod +x /usr/local/bin/flux + +# Passwordless sudo for the interactive developer user. +# Official ubuntu images ship uid 1000 as user "ubuntu" — rename it. +RUN usermod -l developer ubuntu \ + && groupmod -n developer ubuntu \ + && usermod -d /home/developer -m developer \ + && echo 'developer ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/developer \ + && chmod 440 /etc/sudoers.d/developer + +# SSH hardening: keys only, no root login. +RUN printf '%s\n' \ + 'PasswordAuthentication no' \ + 'KbdInteractiveAuthentication no' \ + 'PermitRootLogin no' \ + 'AllowUsers developer' \ + 'PubkeyAuthentication yes' \ + 'X11Forwarding no' \ + 'AllowTcpForwarding yes' \ + 'ClientAliveInterval 30' \ + 'ClientAliveCountMax 3' \ + > /etc/ssh/sshd_config.d/99-devbox.conf \ + && ssh-keygen -A + +RUN curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash \ + && curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 22 8080 +USER root +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..06e9e62 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# cursor-devbox + +Personal Cursor Remote-SSH workspace image and GitOps for **node1-k3s**. + +- **Image**: `dcr.trinitysan.by/infra/cursor-devbox` +- **Deploy**: `deploy/` (Namespace, StatefulSet, ingress) +- **Flux image automation**: `flux/` (ImageRepository / ImagePolicy / ImageUpdateAutomation) + +Infra on node1 only keeps thin Flux glue (`GitRepository` + Kustomizations + auth SecretAdapters) in `infrastructure/node1-k3s/130-cursor-devbox.yaml`. + +## Access + +| Endpoint | Detail | +|----------|--------| +| SSH | `ssh -p 2222 developer@devbox.trinitysan.by` | +| HTTP health | `https://devbox.trinitysan.by/` | + +## CI + +Gitea Actions (`.gitea/workflows/build-and-push.yaml`) on `master` (Dockerfile / entrypoint changes) and `workflow_dispatch` pushes: + +| Tag | Purpose | +|-----|---------| +| `:latest` | Convenience | +| `:` | Immutable full SHA | +| `:-` | Sortable; elected by Flux ImagePolicy | + +Org secrets: `DCR_USERNAME`, `DCR_PASSWORD` (infra org). + +## Local build + +```bash +docker build -t dcr.trinitysan.by/infra/cursor-devbox:local . +``` + +## Flux update flow + +1. CI pushes `run_number-sha` to DCR +2. ImageRepository scans; ImagePolicy picks highest `run_number` +3. ImageUpdateAutomation commits the new tag into `deploy/020-statefulset.yaml` +4. Kustomization applies; StatefulSet rolls (active SSH sessions drop) + +Requires a Gitea PAT with **read+write** on this repo (`GITEA_CURSOR_DEVBOX_GITOPS_TOKEN` on node1, see infra `060-external-secrets.yaml.template`). diff --git a/deploy/010-namespace-secrets.yaml b/deploy/010-namespace-secrets.yaml new file mode 100644 index 0000000..2e97137 --- /dev/null +++ b/deploy/010-namespace-secrets.yaml @@ -0,0 +1,61 @@ +# Namespace, DCR pull secret, SSH allowlist, Service. +# Reconciled by Flux Kustomization cursor-devbox (path ./deploy) on node1-k3s. + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cursor-devbox + labels: + name: cursor-devbox + annotations: + linkerd.io/inject: disabled + +--- +# Replicate federated DCR RO credentials for kubelet imagePullSecrets. +apiVersion: secret-adapter.trinitysan.by/v1 +kind: SecretAdapter +metadata: + name: dcr-registry-auth-ro + namespace: cursor-devbox +spec: + name: dcr-registry-auth-ro + namespace: cursor-devbox + type: kubernetes.io/dockerconfigjson + waitForSources: true + syncInterval: 60 + stringData: + .dockerconfigjson: ${federated-secrets.dcr-registry-auth-ro.".dockerconfigjson"} + +--- +# Hardcoded allowlist: laptop SSH public keys (not secrets). +apiVersion: v1 +kind: ConfigMap +metadata: + name: cursor-devbox-authorized-keys + namespace: cursor-devbox +data: + authorized_keys: | + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDbyPCd5RhlTZYCIgmrSGR6ugnwI513vzHf0qvUP3pGf8mYVd68G6CLu9OXJxFv+LKo2eT8H3ToxLpuG0iiIWz+4rJrNXTJInU2lqrcMsHhLoo1XCs7OAARYiIp7Pl4XKcLdbiXwdL2XUsHImv7cjmaD6jGJ3yR+q9t6jORQEB/h6nUc0MMkNKLIrUyJSlZtWRp22ymGnEqiwKl9doukii6QBhY1k+i9oW09ln6gbFTol9KoONkJ74b2iKw5abZwyjjXuXaE1b98/nmI30MYs8VMJC/w9pEZbmVEflb1k4+C1IE5llwmievjNWpWlZt38uzvGTLbiC9mMIewwFm4UsT5ASLpYKBdFrCaK4JIOTCdJt+SMInkzz0rArCNPMBwZIjBgCFSM33nJsX4gbf/l/rnVtLjXr86rF5s9SOSVyQmjPJoBigqyGw45H2CrB7iTqqWGCpPTnXbzzspiIQGYXLW5+5/4wARCPO0KXYcPgg4Vzh5z+dMcFKJvc8XhocZCc= aaa@LAPTOP-08PG1IL5 + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDK+dk9MtPgRZr7638SASpbJK7z+XkmBesXRlozTFqvT0X6gKk+75kyqNR2W+ZXDwiiWuXS2t9Dbdk9Xpuj2uWaUE0D+n3NOFEQV27Zd1ZTMeLlf3ukSLhb7un0pFHXQXvPn2Iwhh5zCyTDvIWU1rHUgBmakmxoRZ6zwJdsZX/yZHurts//7Dvb2xKKYAq4MFMAB+s6Ch80q6kCnHcByJVFyVLfOUVcNzHGUQbbj4pft2QYiTVxqTBZdvdWQvdBRkKjV2oK/mRUfGsdwmk+ldT65hdcFjbG3oG4hD3g2+AJC+/vN/xqlCeht/TU/Ei4GMF3UB3ank7z1rsGbU1T5lDoFWBWMg+tgw7ZdbYD+HIi/kuQU3FKM2xekB2fgpzuu5GQynjflygBmuQQp5/HAJDoxrkt+Yn79QX6aan8xXcHnt600Bm7SWfnsCl6BpcaD/8f2Lr8CcFCN5pcG020gvLSVdok71dB27NQ/2HOax1i0WQTZVjDrL6kMwRb37njr/k= svv@LAPTOP-08PG1IL5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: cursor-devbox + namespace: cursor-devbox + annotations: + service.kubernetes.io/topology-mode: Auto +spec: + selector: + app: cursor-devbox + ports: + - name: ssh + port: 22 + targetPort: ssh + protocol: TCP + - name: http + port: 8080 + targetPort: http + protocol: TCP diff --git a/deploy/020-statefulset.yaml b/deploy/020-statefulset.yaml new file mode 100644 index 0000000..c0c1058 --- /dev/null +++ b/deploy/020-statefulset.yaml @@ -0,0 +1,113 @@ +# StatefulSet: OpenSSH workspace + DinD sidecar. +# Image tag is updated by Flux ImageUpdateAutomation (SHA-bearing run_number-sha tags). + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: cursor-devbox + namespace: cursor-devbox +spec: + serviceName: cursor-devbox + replicas: 1 + selector: + matchLabels: + app: cursor-devbox + template: + metadata: + labels: + app: cursor-devbox + annotations: + linkerd.io/inject: disabled + spec: + terminationGracePeriodSeconds: 30 + imagePullSecrets: + - name: dcr-registry-auth-ro + containers: + - name: ssh + image: dcr.trinitysan.by/infra/cursor-devbox:latest # {"$imagepolicy": "flux-system:cursor-devbox"} + imagePullPolicy: IfNotPresent + ports: + - name: ssh + containerPort: 22 + - name: http + containerPort: 8080 + env: + - name: DOCKER_HOST + value: tcp://127.0.0.1:2375 + - name: HOME + value: /home/developer + securityContext: + allowPrivilegeEscalation: true + capabilities: + add: + - SYS_PTRACE + resources: + requests: + cpu: "750m" + memory: 3Gi + limits: + cpu: "4" + memory: 8Gi + readinessProbe: + tcpSocket: + port: ssh + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: ssh + initialDelaySeconds: 15 + periodSeconds: 20 + volumeMounts: + - name: home + mountPath: /home/developer + subPath: home + - name: authorized-keys + mountPath: /etc/ssh-authorized-keys + readOnly: true + - name: dind + image: docker:27-dind + imagePullPolicy: IfNotPresent + args: + - --host=tcp://0.0.0.0:2375 + - --tls=false + env: + - name: DOCKER_TLS_CERTDIR + value: "" + securityContext: + privileged: true + resources: + requests: + cpu: "250m" + memory: 1Gi + limits: + cpu: "2" + memory: 4Gi + readinessProbe: + tcpSocket: + port: 2375 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: home + mountPath: /var/lib/docker + subPath: docker + - name: dind-run + mountPath: /var/run + volumes: + - name: authorized-keys + configMap: + name: cursor-devbox-authorized-keys + - name: dind-run + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: home + spec: + accessModes: + - ReadWriteOnce + storageClassName: local-storage + resources: + requests: + storage: 30Gi diff --git a/deploy/030-ingress.yaml b/deploy/030-ingress.yaml new file mode 100644 index 0000000..efe9030 --- /dev/null +++ b/deploy/030-ingress.yaml @@ -0,0 +1,33 @@ +# Traefik TCP SSH + TrinityIngress HTTP health. + +--- +# Traefik TCP :2222 → OpenSSH in the pod (plain SSH; HostSNI catch-all). +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + name: cursor-devbox-ssh + namespace: cursor-devbox +spec: + entryPoints: + - ssh + routes: + - match: HostSNI(`*`) + services: + - name: cursor-devbox + port: 22 + +--- +# DNS A record + HTTPS front for the tiny HTTP health page; SSH uses Traefik TCP :2222. +apiVersion: ingress.trinitysan.by/v1 +kind: TrinityIngress +metadata: + name: cursor-devbox + namespace: cursor-devbox +spec: + hostname: devbox.trinitysan.by + targetTraefikIP: "91.210.106.53" + serviceRef: + namespace: cursor-devbox + name: cursor-devbox + port: 8080 + certificateReclaimPolicy: Retain diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..18e4cf4 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -euo pipefail + +HOME_DIR=/home/developer +HOST_KEY_DIR="${HOME_DIR}/.ssh-host" +AUTH_KEYS_SRC=/etc/ssh-authorized-keys/authorized_keys + +mkdir -p "${HOME_DIR}/.ssh" "${HOME_DIR}/workspace" "${HOST_KEY_DIR}" + +# Persist host keys on the PVC so SSH fingerprints stay stable across restarts. +if [ ! -f "${HOST_KEY_DIR}/ssh_host_ed25519_key" ]; then + ssh-keygen -t ed25519 -f "${HOST_KEY_DIR}/ssh_host_ed25519_key" -N "" -q +fi +if [ ! -f "${HOST_KEY_DIR}/ssh_host_rsa_key" ]; then + ssh-keygen -t rsa -b 4096 -f "${HOST_KEY_DIR}/ssh_host_rsa_key" -N "" -q +fi + +cat > /etc/ssh/sshd_config.d/98-hostkeys.conf <> /etc/environment +if [ ! -f "${HOME_DIR}/.profile" ] || ! grep -q 'DOCKER_HOST' "${HOME_DIR}/.profile" 2>/dev/null; then + printf '\nexport DOCKER_HOST=tcp://127.0.0.1:2375\n' >> "${HOME_DIR}/.profile" + chown developer:developer "${HOME_DIR}/.profile" +fi + +# Tiny HTTP for TrinityIngress / health (Traefik terminates TLS). +python3 -m http.server 8080 --bind 0.0.0.0 --directory /usr/share/devbox-http >/tmp/http-8080.log 2>&1 & + +exec /usr/sbin/sshd -D -e diff --git a/flux/image-automation.yaml b/flux/image-automation.yaml new file mode 100644 index 0000000..8124a01 --- /dev/null +++ b/flux/image-automation.yaml @@ -0,0 +1,58 @@ +# Flux image automation for cursor-devbox (applied into flux-system on node1). +# GitRepository cursor-devbox is defined in infra glue (130-cursor-devbox.yaml). +# CI tags: latest, , - — policy elects the last form. + +--- +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImageRepository +metadata: + name: cursor-devbox + namespace: flux-system +spec: + image: dcr.trinitysan.by/infra/cursor-devbox + interval: 1m0s + secretRef: + name: dcr-registry-auth-ro + +--- +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImagePolicy +metadata: + name: cursor-devbox + namespace: flux-system +spec: + imageRepositoryRef: + name: cursor-devbox + filterTags: + pattern: '^(?P[0-9]+)-[0-9a-f]{40}$' + extract: '$n' + policy: + numerical: + order: asc + +--- +apiVersion: image.toolkit.fluxcd.io/v1 +kind: ImageUpdateAutomation +metadata: + name: cursor-devbox + namespace: flux-system +spec: + interval: 5m0s + sourceRef: + kind: GitRepository + name: cursor-devbox + git: + checkout: + ref: + branch: master + commit: + author: + email: fluxcdbot@users.noreply.gitea.trinitysan.by + name: fluxcdbot + messageTemplate: | + chore(deploy): update cursor-devbox image to {{range .Updated.Images}}{{.}}{{end}} + push: + branch: master + update: + path: ./deploy + strategy: Setters