Initial cursor-devbox: image, deploy manifests, and Flux SHA automation.
build-and-push / build (push) Failing after 1m42s

Owns Dockerfile/CI and node1 workload YAML; ImagePolicy elects run_number-sha tags and ImageUpdateAutomation commits them into deploy/.
This commit is contained in:
Ubuntu
2026-09-20 12:57:44 +00:00
commit 05ea40562c
8 changed files with 459 additions and 0 deletions
+33
View File
@@ -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
+76
View File
@@ -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"]
+43
View File
@@ -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 |
| `:<git-sha>` | Immutable full SHA |
| `:<run_number>-<git-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`).
+61
View File
@@ -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
+113
View File
@@ -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
+33
View File
@@ -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
Executable
+42
View File
@@ -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 <<EOF
HostKey ${HOST_KEY_DIR}/ssh_host_ed25519_key
HostKey ${HOST_KEY_DIR}/ssh_host_rsa_key
EOF
if [ -f "${AUTH_KEYS_SRC}" ]; then
install -m 600 -o developer -g developer "${AUTH_KEYS_SRC}" "${HOME_DIR}/.ssh/authorized_keys"
fi
# sshd StrictModes rejects auth if $HOME is group/world-writable.
chmod 755 "${HOME_DIR}"
chmod 700 "${HOME_DIR}/.ssh"
chown -R developer:developer "${HOME_DIR}"
# DinD sidecar listens on TCP; ensure SSH login shells see DOCKER_HOST.
grep -q '^DOCKER_HOST=' /etc/environment 2>/dev/null \
|| echo 'DOCKER_HOST=tcp://127.0.0.1:2375' >> /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
+58
View File
@@ -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, <sha>, <run_number>-<sha> — 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<n>[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