!16 [Feature] DevContainer 适配 Docker

* [Chore] format code style
* [Chore] go mod tidy
* remove deprecated config
* Update services/devstar_devcontainer/docker_agent/AssignDevcontainerCr…
This commit is contained in:
xinitx
2024-10-31 03:00:11 +00:00
repo.diff.committed_by 戴明辰
repo.diff.parent c92eeecf06
repo.diff.commit 8d5b34e07e
repo.diff.stats_desc%!(EXTRA int=13, int=423, int=46)

repo.diff.view_file

@@ -3,49 +3,61 @@
DevStar Studio 是 Gitea 发行版
## 1. 快速开始
编译、打包成为镜像:代码目录执行 `make docker` 命令
### 1.1 单机版
### 1.1 单机版部署
#### 1.1.1 部署
使用docker-compose部署DevStar Studio需要参考官方网站
https://docs.gitea.com/zh-cn/next/installation/install-with-docker-rootless
**Linux部署**
- 数据库准备可选可使用sqlite参考网址https://docs.gitea.com/zh-cn/installation/database-prep
- 环境准备docker、git
- 下载Linux可执行文件压缩包-linux **tar -xzvf** 解压缩得到可执行文件Linux部署命令参考网址https://docs.gitea.com/zh-cn/installation/install-from-binary
**Docker镜像部署**
- 数据库准备可选可使用sqlite参考网址https://docs.gitea.com/zh-cn/installation/database-prep
- 环境准备docker、git
- 下载镜像文件压缩包(-docker **tar -xzvf** 解压缩得到镜像文件
下列以SQLite db本地最小安装为例首先初次启动复制配置信息
```bash
docker run \
--restart=always \
--name devstar-studio \
-d \
-p 3000:3000 \
devstar-studio:latest
# 加载镜像
docker load -i <镜像文件名>
# Windows: docker desktop需打开Expose daemon on tcp://localhost:2375 without TLS
docker run --restart=always --name <容器名> -d -p 3000:3000 devstar-studio:latest
# linux: docker运行容器后面修改linux主机 /var/run/docker.sock 权限为 666
docker run --restart=always --name <容器名> -d -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock devstar-studio:latest
sudo chmod 666 /var/run/docker.sock
```
浏览器打开 http://localhost:3000执行安装操作记录下列路径名记录下来准备后续映射文件夹
需要复制的配置信息如下:
- 数据库文件路径名字
- 仓库根路径名字
- LFS根目录名字
- 日志路径名字
#### 1.1.2 安装和配置
**!先运行并通过首页安装,完成后再进行配置!**
将容器内 `/etc/gitea/app.ini` 复制出来,参考[官网文档](https://docs.gitea.com/administration/config-cheat-sheet) 进行自定义
```bash
# linux docker 需要把配置文件复制出来修改Windows: docker desktop在容器界面-file找到/etc/gitea/app.ini修改
docker cp devstar-studio:/etc/gitea/app.ini ./app.ini
docker cp ./app.ini devstar-studio:/etc/gitea/app.ini
```
然后 添加/修改 下列小节键:
```ini
[wechat]
WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS=60
WECHAT_OFFICIAL_ACCOUNT_APP_ID=<微信公众号APPID>
WECHAT_OFFICIAL_ACCOUNT_APP_SECRET=<微信公众号SECRET>
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN = <微信公众号自定义Token>
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY = <微信公众号AES加密密钥>
添加/修改 下列小节键:
```ini
[database]
CHARSET_COLLATION = utf8mb4_bin
[devstar.ssh_key_pair]
KEY_SIZE = <写入希望生成的SSH密钥长度比如 4096 ,默认值 2048>
[cors]
CONTENT_SECURITY_POLICY = default-src 'self' data: 'unsafe-inline' https://mp.weixin.qq.com; img-src * data:
ENABLED = true
ALLOW_DOMAIN = *
METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
MAX_AGE = 10m
[ui.admin]
;; Dev Container 分页参数(每页展示 DevContainer 个数),若未指定,默认值 50
@@ -55,28 +67,21 @@ DEV_CONTAINERS_PAGING_NUM = 50
ENABLED = true
AGENT = docker
TIMEOUT_SECONDS = 120
HOST = 127.0.0.1
[devstar.ssh_key_pair]
KEY_SIZE = <写入希望生成的SSH密钥长度比如 4096 ,默认值 2048>
;; 修改为部署host地址
HOST = 192.168.88.88
;; docker desktop必须添加linux docker可删除
DOCKER_HOST = tcp://host.docker.internal:2375
```
正式部署单机版
```bash
docker stop devstar-studio && docker rm devstar-studio
#### 1.1.3 单机版使用
1. 添加publickey用户-设置-SSH / GPG 密钥-管理 SSH 密钥windows一般在C:/用户/用户名/.ssh目录下
2. 创建仓库,创建.devcontainer/devcontainer.json并且 **添加 image 字段**,例如:"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04"
3. 仓库内-开发容器-创建开发容器
```bash
docker run \
--restart=always \
--name devstar-studio \
-d \
-p 3000:3000 \
-p 2222:2222 \
-v 本地数据库文件路径:容器内数据库文件路径 \
-v 本地仓库根路径:容器内仓库根路径 \
-v 本地LFS根目录:容器内LFS路径 \
-v 本地日志路径:容器内日志路径 \
devstar-studio:latest
```
### 1.2 Kubernetes 集群部署

10
go.mod
repo.diff.view_file

@@ -30,6 +30,8 @@ require (
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.2
@@ -131,6 +133,14 @@ require (
xorm.io/xorm v1.3.8
)
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // 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/morikuni/aec v1.0.0 // indirect
)
require (
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.7 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect

19
go.sum
repo.diff.view_file

@@ -94,6 +94,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
@@ -289,6 +291,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/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=
@@ -299,6 +303,14 @@ 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=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
@@ -738,12 +750,16 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
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.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -1202,6 +1218,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1455,6 +1472,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

repo.diff.view_file

@@ -59,6 +59,7 @@ type DevcontainerType struct {
DefaultGitBranchName string
DefaultDevcontainerImageName string
DockerHost string
}
type SSHKeyPairType struct {

repo.diff.view_file

@@ -727,6 +727,15 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
// verifyAuthWithOptions checks authentication according to options
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// 检查是否为 OPTIONS 请求
if ctx.Req.Method == "OPTIONS" {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
ctx.Resp.WriteHeader(http.StatusOK)
return
}
// Check prohibit login users.
if ctx.IsSigned {
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {

repo.diff.view_file

@@ -18,6 +18,15 @@ import (
)
func goGet(ctx *context.Context) {
// 检查是否为 OPTIONS 请求
if ctx.Req.Method == "OPTIONS" {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
ctx.Resp.WriteHeader(http.StatusOK)
return
}
if ctx.Req.Method != "GET" || len(ctx.Req.URL.RawQuery) < 8 || ctx.FormString("go-get") != "1" {
return
}

repo.diff.view_file

@@ -40,7 +40,8 @@ func OpenDevcontainerService(ctx *gitea_web_context.Context, opts *devcontainer_
}
}
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
openDevcontainerAbstractAgentVO.NodePortAssigned = opts.Port
default:
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer",

repo.diff.view_file

@@ -1,6 +1,7 @@
package devstar_devcontainer
import (
"code.gitea.io/gitea/services/devstar_devcontainer/docker_agent"
"context"
"encoding/json"
"fmt"
@@ -276,6 +277,8 @@ func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devst
switch setting.Devstar.Devcontainer.Agent {
case setting.DEVCONTAINER_AGENT_NAME_K8S:
return devcontainer_k8s_agent_service.AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
return docker_agent.AssignDevcontainerDeletionDockerOperator(ctx, devcontainersList)
default:
// 未知 Agent直接报错
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
@@ -300,6 +303,8 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *devcontain
case setting.DEVCONTAINER_AGENT_NAME_K8S:
// k8s Operator
return devcontainer_k8s_agent_service.AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
return docker_agent.AssignDevcontainerCreationDockerOperator(ctx, newDevContainer)
default:
// 未知 Agent直接报错
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{

repo.diff.view_file

@@ -55,6 +55,7 @@ func OpenDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontain
// 2. 调用抽象层获取 DevContainer 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪)
optsOpenDevcontainer := &devcontainer_service_options.OpenDevcontainerAppDispatcherOptions{
Name: devcontainerDetails.DevContainerName,
Port: devcontainerDetails.DevContainerPort,
Wait: opts.Wait,
}
openDevcontainerAbstractAgentVO, err := DevcontainersService.OpenDevcontainerService(ctx, optsOpenDevcontainer)

repo.diff.view_file

@@ -0,0 +1,198 @@
package docker_agent
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
apimachinery_meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var commonSocketPaths = []string{
"/var/run/docker.sock",
"/run/podman/podman.sock",
"$HOME/.colima/docker.sock",
"$XDG_RUNTIME_DIR/docker.sock",
"$XDG_RUNTIME_DIR/podman/podman.sock",
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
func AssignDevcontainerCreationDockerOperator(ctx *context.Context, newDevContainer *devcontainer_service_dto.CreateDevcontainerDTO) error {
log.Info("Docker create container.....")
// 1. 创建docker client
cli, err := CreateDockerClient(ctx)
defer cli.Close()
if err != nil {
return err
}
// 2. 创建容器
opts := &devcontainer_dto.CreateDevcontainerOptions{
Name: newDevContainer.Name,
CreateOptions: apimachinery_meta_v1.CreateOptions{},
Image: newDevContainer.Image,
CommandList: []string{
"sh",
"-c",
"rm -f /etc/ssh/ssh_host_*; ssh-keygen -A ; service ssh start ; while true; do sleep 60; done",
},
SSHPublicKeyList: newDevContainer.SSHPublicKeyList,
GitRepositoryURL: newDevContainer.GitRepositoryURL,
}
// 2. 创建成功,取回集群中的 DevContainer
port, err := CreateDevcontainer(ctx, cli, opts)
if err != nil {
return err
}
// 3. 将分配的 NodePort Service 写回 newDevcontainer供写入数据库进行下一步操作
uint16Value, _ := strconv.ParseUint(port, 10, 16)
newDevContainer.DevcontainerPort = uint16(uint16Value)
return nil
}
func CreateDevcontainer(ctx *context.Context, cli *client.Client, opts *devcontainer_dto.CreateDevcontainerOptions) (string, error) {
if ctx == nil || opts == nil {
return "", fmt.Errorf("拉取镜像失败:环境为空")
}
// 拉取镜像
err := pullImage(cli, opts.Image)
if err != nil {
return "", fmt.Errorf("拉取镜像失败:%v", err)
}
// 创建并启动容器
port, err := createAndStartContainer(cli, opts)
if err != nil {
return "", fmt.Errorf("创建或启动容器失败:%v", err)
}
return port, nil
}
// pullImage 用于拉取指定的 Docker 镜像
func pullImage(cli *client.Client, image string) error {
ctx := context.Background()
resp, err := cli.ImagePull(ctx, image, types.ImagePullOptions{})
if err != nil {
return err
}
defer resp.Close()
_, err = io.Copy(os.Stdout, resp)
return err
}
func createAndStartContainer(cli *client.Client, opts *devcontainer_dto.CreateDevcontainerOptions) (string, error) {
ctx := context.Background()
// 创建容器配置
config := &container.Config{
Image: opts.Image,
Cmd: opts.CommandList,
AttachStdout: true,
AttachStderr: true,
Tty: true,
OpenStdin: true,
ExposedPorts: nat.PortSet{
"22/tcp": struct{}{},
},
}
// 设置容器主机配置
hostConfig := &container.HostConfig{
// PortBindings: nat.PortMap{
// "22/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: strconv.Itoa(int(opts.ServicePort))}}, // 将容器的22端口映射到宿主机的8080端口
// },
ExtraHosts: []string{"host.docker.internal:" + setting.Devstar.Devcontainer.Host},
PublishAllPorts: true,
}
// 创建容器
resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.Name)
if err != nil {
return "", fmt.Errorf("创建或启动容器失败:%v", err)
}
// 启动容器
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
return "", fmt.Errorf("创建或启动容器失败:%v", err)
}
// 将公钥数组转换为字符串
keysString := strings.Join(opts.SSHPublicKeyList, "\n")
log.Info(fmt.Sprintf("keysString %s", keysString))
// 解析原始 URL
parsedURL, err := url.Parse(opts.GitRepositoryURL)
if err != nil {
fmt.Printf("Error parsing URL: %v\n", err)
return "", fmt.Errorf("error pull code from git: %v", err)
}
// 获取主机和端口
hostParts := strings.Split(parsedURL.Host, ":")
port := ""
if len(hostParts) > 1 {
port = hostParts[1]
}
// 修改主机部分
newHost := "host.docker.internal"
if port != "" {
newHost += ":" + port
}
// 设置新的主机
parsedURL.Host = newHost
// 生成新的 URL
newURL := parsedURL.String()
cmd := []string{
"sh", "-c",
"if [ ! -d '/data/workspace' ]; then git clone " + newURL + " /data/workspace && echo \"Git Repository cloned.\"; else echo \"Folder already exists.\"; fi; mkdir -p ~/test; mkdir -p ~/.ssh ; chmod 700 ~/.ssh; echo \"" + keysString + "\" > ~/.ssh/authorized_keys ; chmod 600 ~/.ssh/authorized_keys; ",
}
// 创建 exec 实例
ex, err := cli.ContainerExecCreate(context.Background(), resp.ID, types.ExecConfig{
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return "", fmt.Errorf("启动执行器失败:%v", err)
}
execResp, err := cli.ContainerExecAttach(context.Background(), ex.ID, types.ExecStartCheck{})
if err != nil {
return "", fmt.Errorf("执行命令失败:%v", err)
}
output, err := ioutil.ReadAll(execResp.Reader)
log.Info("Command output:\n%s\n", output)
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), resp.ID)
if err != nil {
return "", fmt.Errorf("获取容器信息失败:%v", err)
}
// 获取端口映射信息
portBindings := containerJSON.NetworkSettings.Ports
for containerPort, bindings := range portBindings {
for _, binding := range bindings {
log.Info("Container Port %s is mapped to Host Port %s on IP %s\n", containerPort, binding.HostPort, binding.HostIP)
if containerPort.Port() == "22" {
return binding.HostPort, nil
}
}
}
return "", fmt.Errorf("未找到映射的端口")
}

repo.diff.view_file

@@ -0,0 +1,50 @@
package docker_agent
import (
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
"code.gitea.io/gitea/modules/log"
"context"
"github.com/docker/docker/api/types"
)
func AssignDevcontainerDeletionDockerOperator(ctx *context.Context, devcontainersList *[]devstar_devcontainer_models.DevstarDevcontainer) error {
log.Info("Docker delete container")
// 1. 创建docker client
cli, err := CreateDockerClient(ctx)
defer cli.Close()
if err != nil {
return err
}
// 获取容器 ID
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
log.Info("获取容器列表失败: %v", err)
}
var containerIDList []string
for _, c := range containers {
log.Info("containersIDList: %v", c.ID)
for _, name := range c.Names {
log.Info("containerNameList: %v", name)
for _, devcontainer := range *devcontainersList {
log.Info("devcontainerName: %v", devcontainer.Name)
if name == "/"+devcontainer.Name {
containerIDList = append(containerIDList, c.ID)
}
}
}
}
// 删除容器
options := types.ContainerRemoveOptions{
Force: true, // 强制删除正在运行的容器
RemoveVolumes: true, // 删除数据卷
RemoveLinks: false, // 删除链接(已弃用)
}
for _, id := range containerIDList {
if err := cli.ContainerRemove(context.Background(), id, options); err != nil {
log.Info("删除%s容器失败: %v", id, err)
}
log.Info("容器 %s 已成功删除\n", id)
}
return nil
}

repo.diff.view_file

@@ -0,0 +1,68 @@
package docker_agent
import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"context"
"fmt"
"github.com/docker/docker/client"
"os"
"path/filepath"
"strings"
)
func CreateDockerClient(ctx *context.Context) (*client.Client, error) {
log.Info("检查 Docker 环境")
// 1. 检查 Docker 环境
dockerSocketPath, err := GetDockerSocketPath()
if err != nil {
return nil, err
}
log.Info("dockerSocketPath: %s", dockerSocketPath)
cli, err := CheckIfDockerRunning(*ctx, dockerSocketPath)
if err != nil {
return nil, err
}
return cli, nil
}
/*
Docker环境路径优先级: 配置文件、环境变量
*/
func GetDockerSocketPath() (string, error) {
if setting.Devstar.Devcontainer.DockerHost != "" {
return setting.Devstar.Devcontainer.DockerHost, nil
}
socket, found := os.LookupEnv("DOCKER_HOST")
if found {
return socket, nil
}
for _, p := range commonSocketPaths {
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
if strings.HasPrefix(p, `\\.\`) {
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
}
return "", fmt.Errorf("daemon Docker Engine socket not found and docker_host config was invalid")
}
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) (*client.Client, error) {
opts := []client.Opt{
client.FromEnv,
}
if configDockerHost != "" {
opts = append(opts, client.WithHost(configDockerHost))
}
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, err
}
_, err = cli.Ping(ctx)
if err != nil {
return nil, fmt.Errorf("cannot ping the docker daemon, is it running? %w", err)
}
return cli, nil
}

repo.diff.view_file

@@ -3,4 +3,5 @@ package options
type OpenDevcontainerAppDispatcherOptions struct {
Name string `json:"name"`
Wait bool `json:"wait"`
Port uint16
}