From d33b8057cba6b7d7dc03a9c6a38be65b473018d1 Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Thu, 3 Jul 2025 08:24:54 +0800 Subject: [PATCH] =?UTF-8?q?devcontainer=20/root=20/etc/ssh=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=81=E4=B9=85=E5=8C=96=20=E4=BF=AE=E6=94=B9open?= =?UTF-8?q?=20with=20vscode=E8=BF=94=E5=9B=9E=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 14 +- go.sum | 17 +- .../devcontainer/templates/statefulset.yaml | 148 +++++++++++++++++- services/devcontainer/devcontainer.go | 129 ++++++++++++++- services/devcontainer/k8s_agent.go | 113 ++++++++++++- 5 files changed, 394 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 77f7d24f80..8d302d062a 100644 --- a/go.mod +++ b/go.mod @@ -129,6 +129,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 + k8s.io/kubectl v0.32.3 mvdan.cc/xurls/v2 v2.5.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.13 @@ -141,22 +142,22 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.22.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/stoewer/go-strcase v1.3.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect @@ -166,7 +167,6 @@ require ( go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -312,8 +312,8 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 + github.com/onsi/ginkgo/v2 v2.23.4 // indirect + github.com/onsi/gomega v1.37.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -385,3 +385,5 @@ exclude github.com/gofrs/uuid v4.0.0+incompatible exclude github.com/goccy/go-json v0.4.11 exclude github.com/satori/go.uuid v1.2.0 + +replace github.com/docker/distribution => github.com/distribution/distribution v2.8.0+incompatible diff --git a/go.sum b/go.sum index e1a48b5dab..e52e1e385a 100644 --- a/go.sum +++ b/go.sum @@ -243,8 +243,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/distribution/distribution v2.8.0+incompatible h1:YKTAHrTIHNQ1HnaTYCudkxWkW9LujmtudCTQh3/5AYk= +github.com/distribution/distribution v2.8.0+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc= github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= @@ -255,8 +255,6 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -382,6 +380,7 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -487,6 +486,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= @@ -637,6 +638,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -655,6 +658,8 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= @@ -712,8 +717,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -1131,6 +1134,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= +k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= diff --git a/modules/k8s/controller/devcontainer/templates/statefulset.yaml b/modules/k8s/controller/devcontainer/templates/statefulset.yaml index 4a11832ae2..ca7c7411d9 100644 --- a/modules/k8s/controller/devcontainer/templates/statefulset.yaml +++ b/modules/k8s/controller/devcontainer/templates/statefulset.yaml @@ -22,20 +22,126 @@ spec: # 安全策略,禁止挂载 ServiceAccount Token automountServiceAccountToken: false volumes: - - name: root-ssh-dir + # 添加 ttyd 共享卷 + - name: ttyd-shared emptyDir: {} initContainers: + # 用户配置初始化 + - name: init-user-config + image: {{.Spec.StatefulSet.Image}} + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - | + echo "=== Checking /target-root directory ===" + ls -la /target-root/ 2>/dev/null || echo "Directory not found" + + # 检查是否为空目录或首次初始化 + file_count=$(find /target-root -maxdepth 1 \( -type f -o -type d \) ! -name '.' ! -name '..' 2>/dev/null | wc -l) + echo "Found $file_count items in /target-root" + + if [ "$file_count" -lt 2 ]; then + echo "Empty or minimal directory detected - initializing user home..." + cp -a /root/. /target-root/ + echo "User config initialized from image defaults" + else + echo "User config already exists - skipping initialization to preserve user data" + echo "Current contents:" + ls -la /target-root/ + fi + volumeMounts: + - name: pvc-devcontainer + mountPath: /target-root + subPath: user-home + + # SSH 配置和公钥初始化 - name: init-root-ssh-dir image: devstar.cn/public/busybox:27a71e19c956 imagePullPolicy: IfNotPresent command: - /bin/sh - -c - - {{range .Spec.StatefulSet.SSHPublicKeyList}} echo "{{.}}" >> /root/.ssh/authorized_keys && {{end}} chmod -R 700 /root/.ssh/ && echo 'SSH Public Key(s) imported.' - # 注意,必须递归设置 ~/.ssh/ 目录下权限 700,否则即使配置了 ~/.ssh/authorized_keys 也不会生效 + - | + # 确保目录存在 + mkdir -p /root/.ssh + mkdir -p /etc/ssh + + # 创建标准的 sshd_config 文件(如果不存在) + if [ ! -f /etc/ssh/sshd_config ]; then + cat > /etc/ssh/sshd_config << 'EOF' + # OpenSSH Server Configuration + Port 22 + AddressFamily any + ListenAddress 0.0.0.0 + + # Host Keys + HostKey /etc/ssh/ssh_host_rsa_key + HostKey /etc/ssh/ssh_host_ecdsa_key + HostKey /etc/ssh/ssh_host_ed25519_key + + # Logging + SyslogFacility AUTH + LogLevel INFO + + # Authentication + LoginGraceTime 2m + PermitRootLogin yes + StrictModes yes + MaxAuthTries 6 + MaxSessions 10 + + PubkeyAuthentication yes + AuthorizedKeysFile .ssh/authorized_keys + + PasswordAuthentication no + PermitEmptyPasswords no + ChallengeResponseAuthentication no + + # Forwarding + X11Forwarding yes + X11DisplayOffset 10 + PrintMotd no + PrintLastLog yes + TCPKeepAlive yes + + # Environment + AcceptEnv LANG LC_* + + # Subsystem + Subsystem sftp /usr/lib/openssh/sftp-server + + # PAM + UsePAM yes + EOF + echo "Created sshd_config" + fi + + # 导入 SSH 公钥(如果不存在) + {{range .Spec.StatefulSet.SSHPublicKeyList}} + if ! grep -q "{{.}}" /root/.ssh/authorized_keys 2>/dev/null; then + echo "{{.}}" >> /root/.ssh/authorized_keys + fi + {{end}} + + # 设置正确的权限 + chmod 755 /root + chmod 700 /root/.ssh/ + chmod 600 /root/.ssh/authorized_keys 2>/dev/null || true + chmod 644 /etc/ssh/sshd_config 2>/dev/null || true + + # 确保文件所有者正确 + chown -R root:root /root/.ssh/ + + echo 'SSH configuration and keys initialized.' volumeMounts: - - name: root-ssh-dir - mountPath: /root/.ssh + - name: pvc-devcontainer + mountPath: /root + subPath: user-home + - name: pvc-devcontainer + mountPath: /etc/ssh + subPath: ssh-host-keys + - name: init-git-repo-dir image: {{.Spec.StatefulSet.Image}} imagePullPolicy: IfNotPresent @@ -46,6 +152,25 @@ spec: volumeMounts: - name: pvc-devcontainer mountPath: /data + subPath: user-data + + # ttyd 二进制文件复制 + - name: init-ttyd + image: tsl0922/ttyd:latest + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - | + echo "Copying ttyd binary to shared volume..." + cp /usr/bin/ttyd /ttyd-shared/ttyd + chmod +x /ttyd-shared/ttyd + echo "ttyd binary copied successfully" + ls -la /ttyd-shared/ttyd + volumeMounts: + - name: ttyd-shared + mountPath: /ttyd-shared + containers: - name: {{.ObjectMeta.Name}} image: {{.Spec.StatefulSet.Image}} @@ -67,8 +192,17 @@ spec: volumeMounts: - name: pvc-devcontainer mountPath: /data - - name: root-ssh-dir - mountPath: /root/.ssh + subPath: user-data + - name: pvc-devcontainer + mountPath: /root + subPath: user-home + - name: pvc-devcontainer + mountPath: /etc/ssh + subPath: ssh-host-keys + # 挂载 ttyd 共享卷 + - name: ttyd-shared + mountPath: /ttyd-shared + # 其他配置保持不变... livenessProbe: exec: command: diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 218bc0f2e9..d1a8af9ff4 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -567,6 +567,13 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai return "", fmt.Errorf("不支持的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent) } + // 加载配置文件 + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) + if err != nil { + log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err) + return "", err + } + log.Info("Get_IDE_TerminalURL: 配置文件加载成功, ROOT_URL=%s", cfg.Section("server").Key("ROOT_URL").Value()) // 构建并返回 URL return "://mengning.devstar/" + "openProject?host=" + devcontainer.RepoName + @@ -575,13 +582,128 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai "&username=" + devcontainer.DevContainerUsername + "&path=" + devcontainer.DevContainerWorkDir + "&access_token=" + access_token + - "&devstar_username=" + devcontainer.RepoOwnerName, nil + "&devstar_username=" + devcontainer.RepoOwnerName + + "&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil } func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error { switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": - return fmt.Errorf("unsupported agent") + log.Info("AddPublicKeyToAllRunningDevContainer: 开始为用户 %s (ID=%d) 的所有运行中容器添加公钥", + user.Name, user.ID) + + // 1. 获取用户的所有 DevContainer + opts := &SearchUserDevcontainerListItemVoOptions{ + Actor: user, + } + userDevcontainersVO, err := GetUserDevcontainersList(ctx, opts) + if err != nil { + log.Error("AddPublicKeyToAllRunningDevContainer: 获取用户容器列表失败: %v", err) + return err + } + + repoDevContainerList := userDevcontainersVO.DevContainers + if len(repoDevContainerList) == 0 { + log.Info("AddPublicKeyToAllRunningDevContainer: 用户 %s 没有任何 DevContainer", user.Name) + return nil + } + + log.Info("AddPublicKeyToAllRunningDevContainer: 找到 %d 个 DevContainer", len(repoDevContainerList)) + + // 2. 获取 K8s 客户端 + k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx) + if err != nil { + log.Error("AddPublicKeyToAllRunningDevContainer: 获取 K8s 客户端失败: %v", err) + return err + } + + // 3. 获取标准 K8s 客户端用于执行命令 + stdClient, err := getStandardKubernetesClient() + if err != nil { + log.Error("AddPublicKeyToAllRunningDevContainer: 获取标准 K8s 客户端失败: %v", err) + return err + } + + // 4. 遍历所有容器,检查状态并添加公钥 + successCount := 0 + errorCount := 0 + + for _, repoDevContainer := range repoDevContainerList { + log.Info("AddPublicKeyToAllRunningDevContainer: 处理容器 %s", repoDevContainer.DevContainerName) + + // 4.1 检查 DevContainer 是否运行 + getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{ + GetOptions: metav1.GetOptions{}, + Name: repoDevContainer.DevContainerName, + Namespace: setting.Devcontainer.Namespace, + Wait: false, + } + + devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, getOpts) + if err != nil { + log.Error("AddPublicKeyToAllRunningDevContainer: 获取容器 %s 状态失败: %v", + repoDevContainer.DevContainerName, err) + errorCount++ + continue + } + + // 4.2 检查容器是否就绪 + if !devcontainerApp.Status.Ready { + log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 未就绪,跳过", + repoDevContainer.DevContainerName) + continue + } + + log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 就绪,开始添加公钥", + repoDevContainer.DevContainerName) + + // 4.3 构建添加公钥的命令 + // 使用更安全的方式添加公钥,避免重复添加 + addKeyCommand := fmt.Sprintf(` + # 确保 .ssh 目录存在 + mkdir -p ~/.ssh + chmod 700 ~/.ssh + + # 检查公钥是否已存在 + if ! grep -Fxq "%s" ~/.ssh/authorized_keys 2>/dev/null; then + echo "%s" >> ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + echo "Public key added successfully" + else + echo "Public key already exists" + fi + + # 验证文件内容 + wc -l ~/.ssh/authorized_keys + `, publicKey, publicKey) + + // 4.4 在容器中执行命令 + err = executeCommandInK8sPod(&ctx, stdClient, + setting.Devcontainer.Namespace, + repoDevContainer.DevContainerName, // 传递 DevContainer 名称而不是 Pod 名称 + repoDevContainer.DevContainerName, // 容器名通常与 DevContainer 名相同 + []string{"/bin/bash", "-c", addKeyCommand}) + + if err != nil { + log.Error("AddPublicKeyToAllRunningDevContainer: 在容器 %s 中执行添加公钥命令失败: %v", + repoDevContainer.DevContainerName, err) + errorCount++ + } else { + log.Info("AddPublicKeyToAllRunningDevContainer: 成功为容器 %s 添加公钥", + repoDevContainer.DevContainerName) + successCount++ + } + } + + log.Info("AddPublicKeyToAllRunningDevContainer: 完成处理 - 成功: %d, 失败: %d", + successCount, errorCount) + + if errorCount > 0 && successCount == 0 { + return fmt.Errorf("所有容器添加公钥都失败了,错误数量: %d", errorCount) + } + + return nil + case setting.DOCKER: cli, err := docker.CreateDockerClient(&ctx) if err != nil { @@ -623,8 +745,9 @@ func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model. } } return nil + default: - return fmt.Errorf("unknown agent") + return fmt.Errorf("unknown agent: %s", setting.Devcontainer.Agent) } } diff --git a/services/devcontainer/k8s_agent.go b/services/devcontainer/k8s_agent.go index 37cb9c1638..ea0906d9c4 100644 --- a/services/devcontainer/k8s_agent.go +++ b/services/devcontainer/k8s_agent.go @@ -1,6 +1,7 @@ package devcontainer import ( + "bytes" "context" "fmt" "strings" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/devcontainer/errors" + v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,6 +26,8 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" + "k8s.io/kubectl/pkg/scheme" ) var k8sGroupVersionResource = schema.GroupVersionResource{ @@ -242,12 +246,24 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine command := []string{ "/bin/bash", "-c", - "rm -f /etc/ssh/ssh_host_* && ssh-keygen -A && service ssh start && " + + "export DEBIAN_FRONTEND=noninteractive && " + "apt-get update -y && " + - "apt-get install -y build-essential cmake git libjson-c-dev libwebsockets-dev && " + - "git clone https://github.com/tsl0922/ttyd.git /tmp/ttyd && " + - "cd /tmp/ttyd && mkdir build && cd build && cmake .. && make && make install && " + - "nohup ttyd -p 7681 -W bash > /dev/null 2>&1 & " + + "apt-get install -y ssh && " + + // 改为条件生成:只有在密钥不存在时才生成 + "if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then " + + " echo 'Generating SSH host keys...' && " + + " ssh-keygen -A && " + + " echo 'SSH host keys generated' ; " + + "else " + + " echo 'SSH host keys already exist' ; " + + "fi && " + + "mkdir -p /var/run/sshd && " + + "/usr/sbin/sshd && " + + "if [ -f /ttyd-shared/ttyd ]; then " + + "mkdir -p /data/workspace && " + + "cd /data/workspace && " + + "/ttyd-shared/ttyd -p 7681 -i 0.0.0.0 --writable bash > /tmp/ttyd.log 2>&1 & " + + "fi && " + "while true; do sleep 60; done", } log.Info("AssignDevcontainerCreation2K8sOperator: Command includes ttyd installation and startup") @@ -642,3 +658,90 @@ func getStandardKubernetesClient() (*kubernetes.Clientset, error) { return stdClient, nil } + +// executeCommandInK8sPod 在 K8s Pod 中执行命令的辅助函数 +func executeCommandInK8sPod(ctx *context.Context, client *kubernetes.Clientset, namespace, devcontainerName, containerName string, command []string) error { + log.Info("executeCommandInK8sPod: 开始为 DevContainer %s 查找对应的 Pod", devcontainerName) + + // 1. 首先根据标签选择器查找对应的 Pod + labelSelector := fmt.Sprintf("app=%s", devcontainerName) + pods, err := client.CoreV1().Pods(namespace).List(*ctx, metav1.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + log.Error("executeCommandInK8sPod: 查找 Pod 失败: %v", err) + return fmt.Errorf("查找 Pod 失败: %v", err) + } + + if len(pods.Items) == 0 { + log.Error("executeCommandInK8sPod: 未找到 DevContainer %s 对应的 Pod", devcontainerName) + return fmt.Errorf("未找到 DevContainer %s 对应的 Pod", devcontainerName) + } + + // 2. 找到第一个运行中的 Pod + var targetPod *v1.Pod + for i := range pods.Items { + pod := &pods.Items[i] + if pod.Status.Phase == v1.PodRunning { + targetPod = pod + break + } + } + + if targetPod == nil { + log.Error("executeCommandInK8sPod: DevContainer %s 没有运行中的 Pod", devcontainerName) + return fmt.Errorf("DevContainer %s 没有运行中的 Pod", devcontainerName) + } + + podName := targetPod.Name + log.Info("executeCommandInK8sPod: 找到运行中的 Pod: %s, 在容器 %s 中执行命令", + podName, containerName) + + // 3. 执行命令 + req := client.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec"). + Param("container", containerName) + + req.VersionedParams(&v1.PodExecOptions{ + Container: containerName, + Command: command, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + // 获取 executor + config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) + if err != nil { + // 如果集群外配置失败,尝试集群内配置 + config, err = rest.InClusterConfig() + if err != nil { + return fmt.Errorf("获取 K8s 配置失败: %v", err) + } + } + + executor, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return fmt.Errorf("创建命令执行器失败: %v", err) + } + + // 执行命令 + var stdout, stderr bytes.Buffer + err = executor.StreamWithContext(*ctx, remotecommand.StreamOptions{ + Stdout: &stdout, + Stderr: &stderr, + }) + + if err != nil { + log.Error("executeCommandInK8sPod: 命令执行失败: %v, stderr: %s", + err, stderr.String()) + return fmt.Errorf("命令执行失败: %v, stderr: %s", err, stderr.String()) + } + + log.Info("executeCommandInK8sPod: 命令执行成功, stdout: %s", stdout.String()) + return nil +}