Compare commits
21 Commits
docs/kuber
...
feature/ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b5d957b9 | ||
|
|
17788afbc2 | ||
|
|
c4bffdeaf7 | ||
|
|
770f295088 | ||
|
|
f6772611e6 | ||
|
|
840c600073 | ||
|
|
b8303e9000 | ||
|
|
56a4039034 | ||
|
|
5891f4f23a | ||
|
|
1ac1caa4a4 | ||
|
|
54b7c8c9fb | ||
|
|
020d24af84 | ||
|
|
1bba6fe4d9 | ||
|
|
dbb295fc7c | ||
|
|
fbb30e8daf | ||
|
|
7be67bf669 | ||
|
|
597f156cee | ||
|
|
96e707b80f | ||
|
|
ba0b6c1b7a | ||
|
|
45d30fd01d | ||
|
|
28adf2541d |
134
DEBUG_FIXES_SUMMARY.md
Normal file
134
DEBUG_FIXES_SUMMARY.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Gitea 工作流在线调试功能 - 故障修复总结
|
||||
|
||||
## 🔧 已修复的问题
|
||||
|
||||
### 1. **路由 404 错误** ✅
|
||||
**问题**:前端 fetch 请求返回 404 Not Found
|
||||
- 原因:fetch URL 构造错误,导致路由不匹配
|
||||
|
||||
**修复方案**:
|
||||
- 后端添加 `DebugAPIURL` 变量传递给前端
|
||||
- 前端使用 `{{.DebugAPIURL}}` 而非手动构造路径
|
||||
- 确保 URL 正确指向 `/repo/actions/debug-api/{sessionID}/run`
|
||||
|
||||
### 2. **CSRF 令牌验证失败** ✅
|
||||
**问题**:HTTP 400 - Invalid CSRF token
|
||||
- 原因:POST 请求没有包含 CSRF 令牌
|
||||
|
||||
**修复方案**:
|
||||
- 后端自动在 `ctx.Data["CsrfToken"]` 设置 CSRF 令牌
|
||||
- 前端从页面上下文读取 `{{.CsrfToken}}`
|
||||
- 在所有 POST 请求头中添加 `X-Csrf-Token: {token}`
|
||||
|
||||
**关键代码**:
|
||||
```javascript
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Csrf-Token': csrfToken,
|
||||
};
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
```
|
||||
|
||||
### 3. **日志显示错误** ✅
|
||||
**问题**:日志容器显示错误或空内容
|
||||
- 原因:refreshLogs 函数尝试从 HTML 页面解析日志,而不是调用 API
|
||||
|
||||
**修复方案**:
|
||||
- 改为调用 ViewPost 接口(POST 到 `/runs/{runIndex}`)
|
||||
- 发送正确的 LogCursors JSON 格式
|
||||
- 从 JSON 响应的 `logs.stepsLog` 提取日志内容
|
||||
- 支持两种日志格式:
|
||||
- `rawOutput`:原始输出
|
||||
- `lines`:逐行输出数组
|
||||
|
||||
**关键代码**:
|
||||
```javascript
|
||||
fetch(`${actionsURL}/runs/${currentRunIndex}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Csrf-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ LogCursors: [] }),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 从 data.logs.stepsLog 提取日志
|
||||
if (data.logs && data.logs.stepsLog) {
|
||||
let logContent = '';
|
||||
data.logs.stepsLog.forEach(stepLog => {
|
||||
if (stepLog.rawOutput) {
|
||||
logContent += stepLog.rawOutput + '\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 📋 文件修改清单
|
||||
|
||||
### 后端文件
|
||||
- **`routers/web/repo/actions/debug.go`**
|
||||
- 添加 `DebugAPIURL` 数据变量
|
||||
- 移除错误的 `ctx.CSRFToken()` 调用(CSRF 令牌自动设置)
|
||||
- 添加调试日志便于故障排查
|
||||
|
||||
### 前端文件
|
||||
- **`templates/repo/actions/debug.tmpl`**
|
||||
- 添加 `csrfToken` 变量初始化
|
||||
- 修改 fetch 请求包含 CSRF header
|
||||
- 修复 `refreshLogs()` 函数调用正确的 API
|
||||
- 改进日志解析逻辑
|
||||
|
||||
## 🚀 使用流程
|
||||
|
||||
1. **访问调试页面**
|
||||
- 进入 Actions 页面 → 选择工作流 → 点击 Debug 按钮
|
||||
|
||||
2. **编辑工作流**
|
||||
- 在左侧编辑器修改 YAML 内容
|
||||
- 选择分支/标签和事件类型
|
||||
- 配置输入参数和环境变量(可选)
|
||||
|
||||
3. **运行工作流**
|
||||
- 点击"Run"按钮
|
||||
- 后端创建 ActionRun 并触发 Runner 执行
|
||||
- 前端自动刷新日志
|
||||
|
||||
4. **查看日志**
|
||||
- 右侧实时显示工作流执行日志
|
||||
- 支持日志复制功能
|
||||
- 点击运行 ID 可查看详细页面
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- ✅ **在线编辑**:直接编辑工作流 YAML
|
||||
- ✅ **灵活配置**:选择分支、事件类型、输入参数、环境变量
|
||||
- ✅ **实时反馈**:自动刷新日志显示执行进度
|
||||
- ✅ **错误处理**:详细的控制台日志便于调试
|
||||
- ✅ **CSRF 保护**:完整的安全验证机制
|
||||
- ✅ **权限管理**:基于 Gitea 的权限系统
|
||||
|
||||
## 🔍 调试提示
|
||||
|
||||
如果遇到问题,查看浏览器控制台日志:
|
||||
- `debugAPIURL` 变量值
|
||||
- `csrfToken` 是否正确
|
||||
- 网络请求状态
|
||||
- 后端返回的错误信息
|
||||
|
||||
## 📌 已知限制
|
||||
|
||||
- 日志刷新间隔 2 秒(可配置)
|
||||
- 仅显示最新日志(不支持历史日志查询)
|
||||
- 工作流会话 24 小时自动过期
|
||||
|
||||
---
|
||||
|
||||
**版本**:v1.0 - Production Ready ✅
|
||||
**最后更新**:2025-11-20
|
||||
152
DEBUG_WORKFLOW_IMPLEMENTATION.md
Normal file
152
DEBUG_WORKFLOW_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Gitea 工作流在线调试功能实现
|
||||
|
||||
## 功能概述
|
||||
|
||||
为 Gitea Actions 工作流系统添加了在线调试功能。用户可以在工作流列表页面点击"调试"按钮,进入一个专用的调试页面,在该页面可以:
|
||||
|
||||
1. **编辑工作流 YAML 内容** - 直接修改工作流定义
|
||||
2. **配置调试参数** - 设置执行的分支/标签、事件类型
|
||||
3. **添加工作流输入** - 为 workflow_dispatch 提供输入参数
|
||||
4. **设置环境变量** - 添加自定义环境变量
|
||||
5. **实时查看日志** - 执行工作流后查看运行日志
|
||||
6. **管理多个调试会话** - 支持创建和删除多个调试会话
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. 数据模型 (`models/actions/debug_session.go`)
|
||||
|
||||
新增 `DebugSession` 模型用于存储调试会话信息:
|
||||
- `ID` - 调试会话唯一ID
|
||||
- `RepoID` - 所属仓库ID
|
||||
- `CreatorID` - 创建者用户ID
|
||||
- `WorkflowID` - 工作流文件名
|
||||
- `WorkflowContent` - 当前编辑的工作流 YAML 内容
|
||||
- `Status` - 会话状态 (draft/running/success/failed/cancelled)
|
||||
- `RunID` - 执行时关联的工作流运行ID
|
||||
- `DebugParams` - 调试参数 JSON (ref、event、inputs、env)
|
||||
- `ErrorMsg` - 错误信息
|
||||
- `ExpiresUnix` - 自动过期时间(24小时)
|
||||
|
||||
### 2. 后端服务 (`services/actions/debug_workflow.go`)
|
||||
|
||||
核心函数:
|
||||
|
||||
**DebugWorkflow()** - 启动调试工作流运行
|
||||
- 解析工作流 YAML 内容
|
||||
- 获取目标提交
|
||||
- 创建 ActionRun 记录
|
||||
- 触发通知系统启动运行
|
||||
|
||||
**GetDebugSessionWorkflowContent()** - 获取工作流原始内容
|
||||
- 从仓库读取工作流文件
|
||||
|
||||
### 3. 路由处理器 (`routers/web/repo/actions/debug.go`)
|
||||
|
||||
**Debug()** - 显示调试页面
|
||||
- 创建新的调试会话
|
||||
- 加载工作流内容
|
||||
- 渲染调试模板
|
||||
|
||||
**API 接口:**
|
||||
- `APIDebugRun()` - 执行调试工作流
|
||||
- `APIDebugSession()` - 获取调试会话状态
|
||||
- `APIDebugSessionUpdate()` - 更新调试会话内容
|
||||
- `APIDebugSessionDelete()` - 删除调试会话
|
||||
|
||||
### 4. 前端界面 (`templates/repo/actions/debug.tmpl`)
|
||||
|
||||
分为左右两栏:
|
||||
|
||||
**左栏 - 调试配置:**
|
||||
- YAML 内容编辑器 (textarea)
|
||||
- 分支/标签选择
|
||||
- 事件类型选择 (push/pull_request/workflow_dispatch 等)
|
||||
- 工作流输入参数配置 (动态添加/删除)
|
||||
- 环境变量配置 (动态添加/删除)
|
||||
- 运行按钮
|
||||
|
||||
**右栏 - 执行日志:**
|
||||
- 运行状态显示
|
||||
- 实时日志输出
|
||||
- 日志自动刷新 (2秒间隔)
|
||||
- 复制日志按钮
|
||||
|
||||
### 5. 路由配置 (`routers/web/web.go`)
|
||||
|
||||
```
|
||||
/repos/{owner}/{repo}/actions/debug?workflow={workflowID}
|
||||
显示调试页面
|
||||
|
||||
/repos/{owner}/{repo}/actions/debug-api/{debugSessionID}/run
|
||||
POST 执行调试工作流
|
||||
|
||||
/repos/{owner}/{repo}/actions/debug-api/{debugSessionID}/session
|
||||
GET 获取调试会话信息
|
||||
|
||||
/repos/{owner}/{repo}/actions/debug-api/{debugSessionID}/update
|
||||
POST 更新调试内容
|
||||
|
||||
/repos/{owner}/{repo}/actions/debug-api/{debugSessionID}/delete
|
||||
POST 删除调试会话
|
||||
```
|
||||
|
||||
### 6. 国际化 (`options/locale/`)
|
||||
|
||||
添加中英文翻译:
|
||||
- `workflow.debug` - 调试工作流
|
||||
- `workflow.content` - 工作流内容
|
||||
- `workflow.ref` - 引用(分支/标签)
|
||||
- `workflow.event` - 事件类型
|
||||
- `workflow.inputs` - 工作流输入
|
||||
- `workflow.env` - 环境变量
|
||||
- `workflow.logs` - 执行日志
|
||||
- 等等...
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **进入工作流列表页面** - `/repos/{owner}/{repo}/actions`
|
||||
2. **选择工作流** - 点击左侧工作流文件
|
||||
3. **点击调试按钮** - "Debug Workflow" 按钮(选中工作流后显示)
|
||||
4. **编辑和配置** - 在调试页面修改工作流内容和参数
|
||||
5. **运行调试** - 点击"Run Workflow"按钮
|
||||
6. **查看日志** - 实时查看工作流执行日志
|
||||
|
||||
## 技术特点
|
||||
|
||||
- **复用现有 Runner** - 使用系统现有的 runner 执行调试工作流
|
||||
- **无需额外基础设施** - 直接利用现有的工作流运行系统
|
||||
- **完整日志支持** - 获得和正常运行相同的完整日志输出
|
||||
- **状态管理** - 支持多个并发调试会话
|
||||
- **自动清理** - 24小时后自动过期的调试会话
|
||||
- **权限检查** - 只有有权限的用户可以创建调试会话
|
||||
|
||||
## 扩展可能性
|
||||
|
||||
未来可以进一步扩展为:
|
||||
- 断点调试支持
|
||||
- 单步执行
|
||||
- 变量查看和修改
|
||||
- 调试历史记录
|
||||
- 性能分析
|
||||
- 集成开发者工具
|
||||
|
||||
## 文件列表
|
||||
|
||||
新增/修改文件:
|
||||
1. `/models/actions/debug_session.go` - 新增
|
||||
2. `/services/actions/debug_workflow.go` - 新增
|
||||
3. `/routers/web/repo/actions/debug.go` - 新增
|
||||
4. `/templates/repo/actions/debug.tmpl` - 新增
|
||||
5. `/templates/repo/actions/list.tmpl` - 修改(添加调试按钮)
|
||||
6. `/routers/web/web.go` - 修改(添加路由)
|
||||
7. `/options/locale/locale_en-US.ini` - 修改(添加翻译)
|
||||
8. `/options/locale/locale_zh-CN.ini` - 修改(添加翻译)
|
||||
|
||||
## 部署注意
|
||||
|
||||
该功能无需特殊部署步骤:
|
||||
- 模型会在启动时自动创建表
|
||||
- 路由会在应用启动时自动注册
|
||||
- 国际化文本会在系统启动时加载
|
||||
|
||||
只需编译和部署更新后的代码即可。
|
||||
214
IMPLEMENTATION_COMPLETE.md
Normal file
214
IMPLEMENTATION_COMPLETE.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# ✅ Gitea 工作流在线调试功能 - 实现总结
|
||||
|
||||
## 📋 任务完成情况
|
||||
|
||||
### 已完成的功能
|
||||
|
||||
✅ **1. 后端数据模型** (`models/actions/debug_session.go`)
|
||||
- 创建 `DebugSession` 模型存储调试会话
|
||||
- 支持 CRUD 操作
|
||||
- 自动过期清理机制
|
||||
- JSON 参数序列化/反序列化
|
||||
|
||||
✅ **2. 后端服务层** (`services/actions/debug_workflow.go`)
|
||||
- `DebugWorkflow()` - 启动调试工作流运行
|
||||
- `GetDebugWorkflowStatus()` - 获取调试运行状态
|
||||
- `GetDebugSessionWorkflowContent()` - 获取工作流原始内容
|
||||
- 完全集成现有 Runner 系统
|
||||
|
||||
✅ **3. API 路由处理器** (`routers/web/repo/actions/debug.go`)
|
||||
- `Debug()` - 显示调试页面视图
|
||||
- `APIDebugRun()` - 执行调试工作流 API
|
||||
- `APIDebugSession()` - 获取调试会话信息 API
|
||||
- `APIDebugSessionUpdate()` - 更新调试内容 API
|
||||
- `APIDebugSessionDelete()` - 删除调试会话 API
|
||||
|
||||
✅ **4. 前端调试页面** (`templates/repo/actions/debug.tmpl`)
|
||||
- 工作流 YAML 编辑器 (textarea)
|
||||
- 分支/标签选择器
|
||||
- 事件类型选择器
|
||||
- 工作流输入参数配置(动态添加/删除)
|
||||
- 环境变量配置(动态添加/删除)
|
||||
- 实时日志查看器(自动刷新)
|
||||
- 日志复制功能
|
||||
|
||||
✅ **5. UI 集成** (`templates/repo/actions/list.tmpl`)
|
||||
- 在工作流列表中添加"Debug Workflow"按钮
|
||||
- 只在选中工作流时显示
|
||||
|
||||
✅ **6. 路由配置** (`routers/web/web.go`)
|
||||
- `/debug` - 显示调试页面
|
||||
- `/debug-api/{debugSessionID}/run` - 运行调试
|
||||
- `/debug-api/{debugSessionID}/session` - 获取会话
|
||||
- `/debug-api/{debugSessionID}/update` - 更新会话
|
||||
- `/debug-api/{debugSessionID}/delete` - 删除会话
|
||||
|
||||
✅ **7. 国际化支持** (locale files)
|
||||
- 英文翻译 (locale_en-US.ini)
|
||||
- 中文简体翻译 (locale_zh-CN.ini)
|
||||
- 所有 UI 文本均已国际化
|
||||
|
||||
## 🔧 技术架构
|
||||
|
||||
```
|
||||
前端请求
|
||||
↓
|
||||
路由处理器 (debug.go)
|
||||
↓
|
||||
后端服务层 (debug_workflow.go)
|
||||
↓
|
||||
数据模型层 (debug_session.go + models/actions/*)
|
||||
↓
|
||||
数据库
|
||||
↓
|
||||
工作流系统 (InsertRun → WorkflowRunStatusUpdate)
|
||||
↓
|
||||
Runner 执行工作流
|
||||
```
|
||||
|
||||
## 📁 文件清单
|
||||
|
||||
### 新增文件
|
||||
- `/models/actions/debug_session.go` - 调试会话数据模型
|
||||
- `/services/actions/debug_workflow.go` - 调试服务实现
|
||||
- `/routers/web/repo/actions/debug.go` - 路由处理器
|
||||
- `/templates/repo/actions/debug.tmpl` - 调试页面模板
|
||||
- `/DEBUG_WORKFLOW_IMPLEMENTATION.md` - 实现文档
|
||||
- `/WORKFLOW_DEBUG_GUIDE.md` - 使用指南
|
||||
|
||||
### 修改的文件
|
||||
- `/templates/repo/actions/list.tmpl` - 添加调试按钮
|
||||
- `/routers/web/web.go` - 添加调试路由
|
||||
- `/options/locale/locale_en-US.ini` - 添加英文翻译
|
||||
- `/options/locale/locale_zh-CN.ini` - 添加中文翻译
|
||||
|
||||
## 🎯 核心特性
|
||||
|
||||
### 特性 1: 完整的工作流编辑
|
||||
- 支持修改任何工作流 YAML 内容
|
||||
- 实时语法验证(通过后端解析)
|
||||
- 清晰的编辑界面
|
||||
|
||||
### 特性 2: 灵活的执行参数
|
||||
- 选择不同分支/标签
|
||||
- 设置不同事件类型
|
||||
- 添加工作流输入参数
|
||||
- 设置环境变量
|
||||
|
||||
### 特性 3: 实时日志反馈
|
||||
- 每 2 秒自动刷新日志
|
||||
- 完整的工作流执行日志
|
||||
- 日志复制功能
|
||||
|
||||
### 特性 4: 会话管理
|
||||
- 多个独立的调试会话
|
||||
- 自动 24 小时过期清理
|
||||
- 草稿状态保存
|
||||
|
||||
### 特性 5: 安全性
|
||||
- 权限检查(需要 Actions 写入权限)
|
||||
- 仓库隔离(只能调试自己仓库的工作流)
|
||||
- 会话所有权验证
|
||||
|
||||
## 🚀 部署步骤
|
||||
|
||||
1. **编译代码**
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
2. **启动应用** - 应用自动创建数据库表和注册路由
|
||||
|
||||
3. **验证功能**
|
||||
- 打开任意仓库的 Actions 页面
|
||||
- 选择工作流文件
|
||||
- 验证"Debug Workflow"按钮可见
|
||||
- 点击按钮进入调试页面
|
||||
|
||||
## 📊 性能考虑
|
||||
|
||||
- **会话管理** - 定期清理过期会话,避免数据库膨胀
|
||||
- **日志查询** - 使用现有的日志系统,无额外开销
|
||||
- **实时刷新** - 2 秒间隔平衡实时性和服务器负载
|
||||
|
||||
## 🔒 安全性
|
||||
|
||||
- ✅ 权限验证 - 需要 Actions 写入权限
|
||||
- ✅ 仓库隔离 - 不能跨仓库调试
|
||||
- ✅ 用户隔离 - 调试会话与创建者关联
|
||||
- ✅ 审计追踪 - 所有调试运行都记录在工作流运行历史中
|
||||
|
||||
## 🎓 使用示例
|
||||
|
||||
### 简单示例:调试构建脚本
|
||||
|
||||
1. 选择 `.gitea/workflows/build.yml`
|
||||
2. 点击 Debug
|
||||
3. 在工作流编辑器中,修改构建命令
|
||||
4. 选择测试分支
|
||||
5. 运行调试
|
||||
6. 查看日志输出
|
||||
|
||||
### 高级示例:测试多种配置
|
||||
|
||||
1. 创建调试会话
|
||||
2. 设置环境变量 `BUILD_TYPE=debug`
|
||||
3. 运行一次
|
||||
4. 更改为 `BUILD_TYPE=release`
|
||||
5. 运行另一次
|
||||
6. 对比两次日志
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
```
|
||||
用户操作
|
||||
│
|
||||
├→ 进入 Actions 页面
|
||||
│ └→ 选择工作流
|
||||
│ └→ 点击 Debug 按钮
|
||||
│
|
||||
└→ 进入调试页面
|
||||
├→ 编辑工作流内容
|
||||
├→ 配置执行参数
|
||||
├→ 点击 Run
|
||||
│ └→ 后端创建 ActionRun
|
||||
│ └→ Runner 执行
|
||||
│ └→ 产生日志
|
||||
│
|
||||
└→ 查看日志
|
||||
├→ 自动刷新
|
||||
├→ 复制日志
|
||||
└→ 查看详细运行页面
|
||||
```
|
||||
|
||||
## ✨ 创新点
|
||||
|
||||
1. **原地调试** - 无需本地环境,直接在 Web UI 调试
|
||||
2. **会话隔离** - 多个独立的调试会话互不影响
|
||||
3. **成本低** - 复用现有 Runner,无额外成本
|
||||
4. **快速反馈** - 实时日志显示,快速迭代
|
||||
5. **完整功能** - 支持所有工作流特性(输入、环境变量等)
|
||||
|
||||
## 📈 后续优化方向
|
||||
|
||||
- 添加工作流语法高亮
|
||||
- 支持工作流模板库
|
||||
- 集成变量和 context 提示
|
||||
- 支持调试历史对比
|
||||
- 支持断点调试(高级)
|
||||
- 性能分析报告
|
||||
|
||||
## ✅ 质量指标
|
||||
|
||||
- ✅ 无编译错误
|
||||
- ✅ 无 lint 警告
|
||||
- ✅ 完整的错误处理
|
||||
- ✅ 规范的代码风格
|
||||
- ✅ 完整的国际化支持
|
||||
- ✅ 安全的权限检查
|
||||
|
||||
---
|
||||
|
||||
**实现完成于**: 2025-11-20
|
||||
**版本**: v1.0
|
||||
**状态**: 生产就绪 ✅
|
||||
23
Makefile
23
Makefile
@@ -917,12 +917,31 @@ generate-manpage: ## generate manpage
|
||||
|
||||
.PHONY: devstar
|
||||
devstar:
|
||||
@if docker pull devstar.cn/devstar/devstar-dev-container:v1.0; then \
|
||||
docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest && \
|
||||
echo "Successfully pulled devstar.cn/devstar/devstar-dev-container:v1.0 taged to latest"; \
|
||||
else \
|
||||
docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer . && \
|
||||
echo "Successfully build devstar.cn/devstar/devstar-dev-container:latest"; \
|
||||
fi
|
||||
@if docker pull devstar.cn/devstar/devstar-runtime-container:v1.0; then \
|
||||
docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest && \
|
||||
echo "Successfully pulled devstar.cn/devstar/devstar-runtime-container:v1.0 taged to latest"; \
|
||||
else \
|
||||
docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer . && \
|
||||
echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \
|
||||
fi
|
||||
@if docker pull devstar.cn/devstar/webterminal:v1.0; then \
|
||||
docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest && \
|
||||
echo "Successfully pulled devstar.cn/devstar/webterminal:v1.0 taged to latest"; \
|
||||
else \
|
||||
docker build --no-cache -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal . && \
|
||||
echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \
|
||||
fi
|
||||
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
docker build -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal .
|
||||
|
||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
||||
|
||||
|
||||
205
WORKFLOW_DEBUG_GUIDE.md
Normal file
205
WORKFLOW_DEBUG_GUIDE.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Gitea 工作流在线调试 - 快速使用指南
|
||||
|
||||
## 🎯 功能特点
|
||||
|
||||
- ✅ **在线编辑工作流** - 无需本地编辑后推送
|
||||
- ✅ **灵活配置参数** - 设置分支、事件、输入、环境变量
|
||||
- ✅ **实时日志查看** - 及时反馈执行结果
|
||||
- ✅ **多调试会话** - 并发调试多个工作流
|
||||
- ✅ **无缝集成** - 使用现有的 Runner 系统
|
||||
|
||||
## 📖 使用步骤
|
||||
|
||||
### 1. 进入工作流页面
|
||||
|
||||
打开仓库的 Actions 页面:
|
||||
```
|
||||
https://your-gitea-instance/repos/{owner}/{repo}/actions
|
||||
```
|
||||
|
||||
### 2. 选择要调试的工作流
|
||||
|
||||
在左侧菜单中点击要调试的工作流文件(如 `deploy.yml`)
|
||||
|
||||
### 3. 点击调试按钮
|
||||
|
||||
在筛选菜单右侧,会看到"Debug Workflow"按钮,点击进入调试页面
|
||||
|
||||
### 4. 配置调试环境
|
||||
|
||||
在调试页面的左侧面板配置:
|
||||
|
||||
#### 🔧 基础配置
|
||||
- **Reference (分支/标签)** - 选择要在哪个分支/标签上执行
|
||||
- **Event Type (事件类型)** - 选择触发事件类型
|
||||
- `push` - 推送事件
|
||||
- `pull_request` - PR 事件
|
||||
- `workflow_dispatch` - 手动触发
|
||||
- 其他...
|
||||
|
||||
#### 📝 工作流输入
|
||||
如果工作流支持 `workflow_dispatch` 输入:
|
||||
1. 点击"Add Input"按钮
|
||||
2. 输入参数名称和值
|
||||
3. 点击运行时会传递这些参数
|
||||
|
||||
示例:
|
||||
```
|
||||
Input Name: environment
|
||||
Input Value: production
|
||||
|
||||
Input Name: version
|
||||
Input Value: 1.0.0
|
||||
```
|
||||
|
||||
#### 🔐 环境变量
|
||||
添加自定义环境变量:
|
||||
1. 点击"Add Environment"按钮
|
||||
2. 输入环境变量名和值
|
||||
3. 工作流执行时可以访问这些变量
|
||||
|
||||
示例:
|
||||
```
|
||||
ENV Name: DEBUG
|
||||
ENV Value: true
|
||||
|
||||
ENV Name: LOG_LEVEL
|
||||
ENV Value: debug
|
||||
```
|
||||
|
||||
#### 📄 工作流内容
|
||||
直接在编辑框中修改工作流 YAML 内容。支持修改:
|
||||
- job 定义
|
||||
- step 定义
|
||||
- 条件表达式
|
||||
- 其他任何工作流语法
|
||||
|
||||
### 5. 执行调试
|
||||
|
||||
点击"Run Workflow"按钮启动调试:
|
||||
- 工作流会立即开始执行
|
||||
- 状态面板会显示运行链接
|
||||
|
||||
### 6. 查看实时日志
|
||||
|
||||
在右侧面板查看:
|
||||
- 📊 **执行日志** - 实时更新(每2秒刷新)
|
||||
- 🔗 **运行链接** - 点击查看完整运行页面
|
||||
- 📋 **复制日志** - 复制所有日志内容
|
||||
|
||||
## 💡 常见场景
|
||||
|
||||
### 场景 1: 调试部署脚本错误
|
||||
|
||||
1. 选择 `deploy.yml` 工作流
|
||||
2. 点击调试
|
||||
3. 修改部署步骤中有问题的命令
|
||||
4. 在 ref 选择 `staging` 分支
|
||||
5. 设置环境变量 `ENVIRONMENT=staging`
|
||||
6. 运行调试
|
||||
7. 查看日志找出问题
|
||||
8. 修改代码并重新测试
|
||||
|
||||
### 场景 2: 测试新的工作流输入
|
||||
|
||||
1. 编辑 `test.yml` 工作流
|
||||
2. 点击调试
|
||||
3. 添加测试输入参数:
|
||||
- `test_level: smoke`
|
||||
- `parallel: true`
|
||||
4. 运行调试
|
||||
5. 验证输入是否正确传递
|
||||
|
||||
### 场景 3: 验证不同环境下的行为
|
||||
|
||||
1. 调试工作流
|
||||
2. 分别设置环境变量:
|
||||
- 第一次:`ENV=dev`
|
||||
- 第二次:`ENV=staging`
|
||||
- 第三次:`ENV=production`
|
||||
3. 对比三次运行的日志
|
||||
4. 确保各环境行为正确
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 权限要求
|
||||
- 需要对仓库有 **Actions 写入权限**
|
||||
- 只有仓库管理员可以创建调试会话
|
||||
|
||||
### 会话管理
|
||||
- 每个调试会话会自动保存编辑的工作流内容
|
||||
- 调试会话 24 小时后自动过期并清理
|
||||
- 可以创建多个调试会话,互不影响
|
||||
|
||||
### 工作流执行
|
||||
- 调试使用的是真实的 Runner,和正常运行一样
|
||||
- 如果工作流有副作用(如部署、数据库操作),请谨慎
|
||||
- 建议在测试分支上进行调试
|
||||
|
||||
### 日志显示
|
||||
- 日志实时更新,可能有 1-2 秒延迟
|
||||
- 日志存储有保留期,超期会被清理
|
||||
- 如需保存日志,请及时复制
|
||||
|
||||
## 🔍 调试技巧
|
||||
|
||||
### 技巧 1: 分步验证
|
||||
```yaml
|
||||
jobs:
|
||||
debug:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Print environment
|
||||
run: |
|
||||
echo "Branch: ${{ github.ref }}"
|
||||
echo "Event: ${{ github.event_name }}"
|
||||
echo "DEBUG is: $DEBUG"
|
||||
```
|
||||
|
||||
### 技巧 2: 添加详细日志
|
||||
```yaml
|
||||
steps:
|
||||
- name: Run with debug output
|
||||
run: |
|
||||
set -x # Print each command
|
||||
my-command --verbose
|
||||
```
|
||||
|
||||
### 技巧 3: 条件调试
|
||||
在工作流中添加调试步骤,使用条件控制:
|
||||
```yaml
|
||||
- name: Debug output
|
||||
if: env.DEBUG == 'true'
|
||||
run: |
|
||||
# 调试命令
|
||||
env
|
||||
pwd
|
||||
ls -la
|
||||
```
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如遇到问题:
|
||||
|
||||
1. **检查权限** - 确保你有 Actions 写入权限
|
||||
2. **查看日志** - 详细的日志通常能指出问题
|
||||
3. **简化工作流** - 从最简单的步骤开始测试
|
||||
4. **查看文档** - Gitea Actions 官方文档和 GitHub Actions 文档
|
||||
|
||||
## 🚀 最佳实践
|
||||
|
||||
✅ **推荐做法**
|
||||
- 在非生产分支上调试
|
||||
- 先用 echo 验证变量和路径
|
||||
- 逐步扩展工作流复杂度
|
||||
- 保存成功的工作流配置
|
||||
|
||||
❌ **避免做法**
|
||||
- 不要在生产分支上运行有风险的调试
|
||||
- 不要频繁修改无关工作流
|
||||
- 不要忽视失败的调试运行
|
||||
- 不要保留临时的调试代码在最终版本
|
||||
|
||||
---
|
||||
|
||||
**提示**: 该调试功能专为开发者设计,可以大幅加快 CI/CD 流程的故障排查和优化。充分利用它可以显著提高开发效率!
|
||||
@@ -12,6 +12,12 @@ RUN apk --no-cache add \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# To acquire Gitea dev container:
|
||||
# $ docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer .
|
||||
# $ docker build -t devstar.cn/devstar/devstar-dev-container:v1.0 -f docker/Dockerfile.devContainer .
|
||||
# $ docker login devstar.cn
|
||||
# $ docker push devstar.cn/devstar/devstar-dev-container:v1.0
|
||||
# $ docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest
|
||||
# $ docker push devstar.cn/devstar/devstar-dev-container:latest
|
||||
|
||||
|
||||
# Release Notes:
|
||||
# v1.0 - Initial release
|
||||
|
||||
@@ -19,6 +19,12 @@ RUN apk --no-cache add \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# To acquire Gitea base runtime container:
|
||||
# $ docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer .
|
||||
# $ docker build -t devstar.cn/devstar/devstar-runtime-container:v1.0 -f docker/Dockerfile.runtimeContainer .
|
||||
# $ docker login devstar.cn
|
||||
# $ docker push devstar.cn/devstar/devstar-runtime-container:v1.0
|
||||
# $ docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest
|
||||
# $ docker push devstar.cn/devstar/devstar-runtime-container:latest
|
||||
|
||||
|
||||
# Release Notes:
|
||||
# v1.0 - Initial release
|
||||
|
||||
@@ -37,4 +37,14 @@ RUN apt-get update && \
|
||||
apt remove --purge curl -y && apt autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/home/webTerminal/build/ttyd", "-W", "bash"]
|
||||
CMD ["/home/webTerminal/build/ttyd", "-W", "bash"]
|
||||
|
||||
# To acquire devstar.cn/devstar/webterminal:latest:
|
||||
# $ docker build --no-cache -t devstar.cn/devstar/webterminal:v1.0 -f docker/Dockerfile.webTerminal .
|
||||
# $ docker login devstar.cn
|
||||
# $ docker push devstar.cn/devstar/webterminal:v1.0
|
||||
# $ docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest
|
||||
# $ docker push devstar.cn/devstar/webterminal:latest
|
||||
|
||||
# Release Notes:
|
||||
# v1.0 - Initial release https://devstar.cn/devstar/webTerminal/commit/2bf050cff984d6e64c4f9753d64e1124fc152ad7
|
||||
@@ -32,7 +32,7 @@ if [ ! -f ${GITEA_CUSTOM}/conf/app.ini ]; then
|
||||
fi
|
||||
|
||||
# Substitute the environment variables in the template
|
||||
APP_NAME=${APP_NAME:-"Gitea: Git with a cup of tea"} \
|
||||
APP_NAME=${APP_NAME:-"DevStar: The Last Mile of Al for R&D"} \
|
||||
RUN_MODE=${RUN_MODE:-"prod"} \
|
||||
DOMAIN=${DOMAIN:-"localhost"} \
|
||||
SSH_DOMAIN=${SSH_DOMAIN:-"localhost"} \
|
||||
|
||||
@@ -26,7 +26,7 @@ if [ ! -f ${GITEA_APP_INI} ]; then
|
||||
fi
|
||||
|
||||
# Substitute the environment variables in the template
|
||||
APP_NAME=${APP_NAME:-"Gitea: Git with a cup of tea"} \
|
||||
APP_NAME=${APP_NAME:-"DevStar: The Last Mile of Al for R&D"} \
|
||||
RUN_MODE=${RUN_MODE:-"prod"} \
|
||||
RUN_USER=${USER:-"git"} \
|
||||
SSH_DOMAIN=${SSH_DOMAIN:-"localhost"} \
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
## Kubernetes 文档入口
|
||||
|
||||
本目录提供从零到一的 Kubernetes 集群安装与常用脚本。建议:先阅读本 README 的概览与快速开始,再按需查看详细版文档。
|
||||
|
||||
### 文档索引
|
||||
|
||||
- **Kubernetes 安装**:`k8s-installtion.md`(分步说明、完整命令与排错)
|
||||
- **Istio 配置**:`istio-hostnetwork-notes.md`(将 Istio IngressGateway 切换为 hostNetwork 模式指南)
|
||||
|
||||
### 快速开始
|
||||
|
||||
在 Master 节点:
|
||||
|
||||
```bash
|
||||
./k8s-step1-prepare-env.sh
|
||||
./k8s-step2-install-containerd.sh
|
||||
./k8s-step3-install-components.sh
|
||||
./k8s-step4-init-cluster.sh
|
||||
./k8s-step5-install-flannel.sh
|
||||
```
|
||||
|
||||
在工作节点加入(参考 `node-join-command.txt` 或运行第 6 步脚本):
|
||||
|
||||
```bash
|
||||
./k8s-step6-join-nodes.sh
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
kubectl get nodes -o wide
|
||||
kubectl get pods -A
|
||||
```
|
||||
|
||||
### 脚本总览
|
||||
|
||||
- 安装流程
|
||||
- `k8s-step1-prepare-env.sh`:环境准备(关闭 swap、内核参数、基础工具)
|
||||
- `k8s-step2-install-containerd.sh`:安装与配置 containerd
|
||||
- `k8s-step3-install-components.sh`:安装 kubeadm/kubelet/kubectl
|
||||
- `k8s-step4-init-cluster.sh`:Master 初始化集群
|
||||
- `k8s-step5-install-flannel.sh`:安装 Flannel CNI(或直接 `kubectl apply -f kube-flannel.yml`)
|
||||
- `k8s-step6-join-nodes.sh`:节点加入集群(使用 `node-join-command.txt`)
|
||||
- `k8s-install-all.sh`:一键顺序执行上述步骤(熟悉流程后使用)
|
||||
|
||||
- 网络与工具
|
||||
- `setup-master-gateway.sh`:Master 网关/NAT 示例配置(按需修改)
|
||||
- `setup-node1.sh`、`setup-node2.sh`:节点路由示例
|
||||
- `k8s-image-pull-and-import.sh`:镜像预拉取/导入(离线或网络慢场景)
|
||||
- `install-kubectl-nodes.sh`:为其他节点安装与配置 kubectl
|
||||
|
||||
### 常见问题
|
||||
|
||||
- 节点 `NotReady`:检查 CNI 是否就绪(`kubectl -n kube-flannel get pods`)、确认已 `swapoff -a`,并查看 `journalctl -u kubelet -f`。
|
||||
- 无法拉取镜像:检查网络/镜像源,可用 `k8s-image-pull-and-import.sh` 预拉取。
|
||||
- `kubectl` 连接异常:确认 `$HOME/.kube/config` 配置与权限。
|
||||
|
||||
### Istio 服务网格配置
|
||||
|
||||
本目录的 Kubernetes 集群安装完成后,如需使用 Istio 作为服务网格和入口网关,请参考:
|
||||
|
||||
#### Istio hostNetwork 模式配置
|
||||
|
||||
**适用场景**:
|
||||
- 只有 master 节点有公网 IP
|
||||
- 需要 Istio IngressGateway 替代 nginx-ingress-controller
|
||||
- 需要 Istio 直接监听宿主机的 80/443 端口
|
||||
|
||||
**详细指南**:请参阅 [`istio-hostnetwork-notes.md`](./istio-hostnetwork-notes.md)
|
||||
|
||||
**快速概览**:
|
||||
1. 安装 Istio(使用 `istioctl install` 或 Helm)
|
||||
2. 按照指南将 `istio-ingressgateway` 切换为 hostNetwork 模式
|
||||
3. 配置 Gateway 和 VirtualService 进行流量路由
|
||||
4. 配置 TLS 证书 Secret
|
||||
|
||||
**注意事项**:
|
||||
- 迁移前确保停止 nginx 或其他占用 80/443 的服务
|
||||
- 需要将 TLS 证书 Secret 复制到 `istio-system` 命名空间
|
||||
- hostNetwork 模式下,Service 类型可以是 `ClusterIP` 或 `LoadBalancer`
|
||||
|
||||
#### 其他 Istio 文档
|
||||
|
||||
- Istio 官方文档:https://istio.io/latest/docs/
|
||||
- Istio 安装指南:https://istio.io/latest/docs/setup/install/
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 为 node1 和 node2 安装 kubectl 脚本
|
||||
# 功能: 从 master 传输 kubectl 二进制文件到其他节点
|
||||
|
||||
echo "==== 为 node1 和 node2 安装 kubectl ===="
|
||||
|
||||
# 定义节点列表
|
||||
NODES=("172.17.0.15:master" "172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 本机 IP 与 SSH 选项
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
# SSH 私钥(可用环境变量 SSH_KEY 覆盖),存在则自动携带
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
|
||||
# 函数:在指定节点执行命令
|
||||
execute_on_node() {
|
||||
local ip="$1"
|
||||
local hostname="$2"
|
||||
local command="$3"
|
||||
local description="$4"
|
||||
|
||||
echo "==== $description on $hostname ($ip) ===="
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
bash -lc "$command"
|
||||
else
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@$ip "$command"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函数:传输文件到指定节点
|
||||
copy_to_node() {
|
||||
local ip="$1"
|
||||
local hostname="$2"
|
||||
local file="$3"
|
||||
echo "传输 $file 到 $hostname ($ip)"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
cp -f "$file" ~/
|
||||
else
|
||||
scp $SSH_OPTS $SSH_ID "$file" ubuntu@$ip:~/
|
||||
fi
|
||||
}
|
||||
|
||||
# 创建 kubectl 安装脚本
|
||||
cat > kubectl-install.sh << 'EOF_INSTALL'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 安装 kubectl ===="
|
||||
|
||||
# 1. 检查是否已安装
|
||||
if command -v kubectl &> /dev/null; then
|
||||
echo "kubectl 已安装,版本: $(kubectl version --client 2>/dev/null | grep 'Client Version' || echo 'unknown')"
|
||||
echo "跳过安装"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2. 安装 kubectl
|
||||
echo "安装 kubectl..."
|
||||
sudo apt update
|
||||
sudo apt install -y apt-transport-https ca-certificates curl
|
||||
|
||||
# 添加 Kubernetes 官方 GPG key
|
||||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
|
||||
# 添加 Kubernetes apt 仓库
|
||||
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
|
||||
|
||||
# 更新包列表并安装 kubectl
|
||||
sudo apt update
|
||||
sudo apt install -y kubectl
|
||||
|
||||
# 3. 验证安装
|
||||
echo "验证 kubectl 安装..."
|
||||
kubectl version --client
|
||||
|
||||
echo "==== kubectl 安装完成 ===="
|
||||
EOF_INSTALL
|
||||
|
||||
chmod +x kubectl-install.sh
|
||||
|
||||
# 为 node1 和 node2 安装 kubectl
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
if [ "$hostname" != "master" ]; then
|
||||
copy_to_node "$ip" "$hostname" "kubectl-install.sh"
|
||||
execute_on_node "$ip" "$hostname" "./kubectl-install.sh" "安装 kubectl"
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理临时文件
|
||||
rm -f kubectl-install.sh
|
||||
|
||||
echo "==== 所有节点 kubectl 安装完成 ===="
|
||||
@@ -1,454 +0,0 @@
|
||||
# Istio IngressGateway 切换为 hostNetwork 模式指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南适用于以下场景:
|
||||
- 只有 master 节点有公网 IP
|
||||
- 需要 Istio IngressGateway 替代 nginx-ingress-controller
|
||||
- 需要 Istio 直接监听宿主机的 80/443 端口
|
||||
|
||||
### 为什么选择 hostNetwork?
|
||||
|
||||
1. **公网 IP 限制**:只有 master 节点有公网 IP,流量入口必须在 master
|
||||
2. **端口一致性**:需要监听标准端口 80/443,与 nginx 保持一致
|
||||
3. **无缝迁移**:无需修改 DNS 或负载均衡器配置
|
||||
|
||||
## 安装 Istio 1.27.1
|
||||
|
||||
### 1. 下载 istioctl
|
||||
|
||||
```bash
|
||||
# 下载 Istio 1.27.1
|
||||
# 根据系统架构选择:x86_64 或 arm64
|
||||
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.27.1 TARGET_ARCH=x86_64 sh -
|
||||
|
||||
# 进入目录
|
||||
cd istio-1.27.1
|
||||
|
||||
# 临时添加到 PATH(当前会话有效)
|
||||
export PATH=$PWD/bin:$PATH
|
||||
|
||||
# 或永久安装到系统路径
|
||||
sudo cp bin/istioctl /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/istioctl
|
||||
|
||||
# 验证安装
|
||||
istioctl version
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `TARGET_ARCH` 根据系统架构选择:`x86_64`(Intel/AMD)或 `arm64`(ARM)
|
||||
- 如果使用临时 PATH,每次新终端会话都需要重新设置
|
||||
- 推荐将 `istioctl` 复制到 `/usr/local/bin` 以便全局使用
|
||||
|
||||
### 2. 安装 Istio
|
||||
|
||||
使用 `default` profile 安装 Istio:
|
||||
|
||||
```bash
|
||||
# 安装 Istio(使用 default profile)
|
||||
istioctl install --set profile=default -y
|
||||
|
||||
# 验证安装
|
||||
kubectl get pods -n istio-system
|
||||
kubectl get svc -n istio-system
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
- `istiod` Pod 应该处于 `Running` 状态
|
||||
- `istio-ingressgateway` Pod 应该处于 `Running` 状态
|
||||
- `istio-egressgateway` Pod 应该处于 `Running` 状态(可选)
|
||||
|
||||
### 3. 验证安装
|
||||
|
||||
```bash
|
||||
# 检查 Istio 组件状态
|
||||
istioctl verify-install
|
||||
|
||||
# 查看 Istio 版本
|
||||
istioctl version
|
||||
|
||||
# 检查所有命名空间的 Istio 资源
|
||||
kubectl get crd | grep istio
|
||||
```
|
||||
|
||||
|
||||
### 4. 卸载 Istio(如需要)
|
||||
|
||||
如果需要卸载 Istio:
|
||||
|
||||
```bash
|
||||
# 卸载 Istio
|
||||
istioctl uninstall --purge -y
|
||||
|
||||
# 删除命名空间
|
||||
kubectl delete namespace istio-system
|
||||
|
||||
# 删除 CRD(可选,会删除所有 Istio 配置)
|
||||
kubectl get crd | grep istio | awk '{print $1}' | xargs kubectl delete crd
|
||||
```
|
||||
|
||||
## 前置检查
|
||||
|
||||
**注意**:如果尚未安装 Istio,请先完成上述"安装 Istio 1.27.1"章节的步骤。
|
||||
|
||||
### 1. 确认集群状态
|
||||
|
||||
```bash
|
||||
# 检查节点
|
||||
kubectl get nodes
|
||||
|
||||
# 检查 Istio 组件(如果已安装)
|
||||
kubectl get pods -n istio-system
|
||||
|
||||
# 检查当前 Service 配置(如果已安装)
|
||||
kubectl get svc istio-ingressgateway -n istio-system
|
||||
|
||||
# 检查 Deployment 配置(如果已安装)
|
||||
kubectl get deploy istio-ingressgateway -n istio-system -o yaml | head -n 50
|
||||
```
|
||||
|
||||
### 2. 释放端口(避免冲突)
|
||||
|
||||
**k3s 环境**:
|
||||
- 如有 traefik,需要停止或释放 80/443
|
||||
- 检查是否有其他服务占用端口:`ss -tlnp | grep -E ':(80|443) '`
|
||||
|
||||
**标准 Kubernetes 环境**:
|
||||
```bash
|
||||
# 停止 nginx-ingress-controller(如果存在)
|
||||
kubectl scale deployment my-release-nginx-ingress-controller \
|
||||
-n nginx-ingress-controller --replicas=0
|
||||
|
||||
# 验证端口已释放
|
||||
ss -tlnp | grep -E ':(80|443) ' || echo "80/443 not listening"
|
||||
```
|
||||
|
||||
## 完整操作步骤
|
||||
|
||||
### 步骤 1:调整 Service(可选)
|
||||
|
||||
如果后续需要接真实 LB,可保留 `LoadBalancer` 类型;为便于本地测试,可先改为 `ClusterIP`:
|
||||
|
||||
```bash
|
||||
# 修改 Service 类型为 ClusterIP
|
||||
kubectl patch svc istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"replace","path":"/spec/type","value":"ClusterIP"}]'
|
||||
|
||||
# 调整端口映射(直通 80/443/15021)
|
||||
kubectl patch svc istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"replace","path":"/spec/ports","value":[
|
||||
{"name":"http","port":80,"targetPort":80,"protocol":"TCP"},
|
||||
{"name":"https","port":443,"targetPort":443,"protocol":"TCP"},
|
||||
{"name":"status-port","port":15021,"targetPort":15021,"protocol":"TCP"}]}]'
|
||||
```
|
||||
|
||||
### 步骤 2:启用 hostNetwork 模式
|
||||
|
||||
```bash
|
||||
# 1. 启用 hostNetwork
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/hostNetwork","value":true}]'
|
||||
|
||||
# 2. 设置 DNS 策略
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/dnsPolicy","value":"ClusterFirstWithHostNet"}]'
|
||||
|
||||
# 3. 绑定到 master 节点(根据实际节点名调整)
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/nodeSelector","value":{"kubernetes.io/hostname":"master"}}]'
|
||||
|
||||
# 4. 添加容忍(如果 master 节点有 control-plane taint)
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/tolerations","value":[{"key":"node-role.kubernetes.io/control-plane","operator":"Exists","effect":"NoSchedule"}]}]'
|
||||
```
|
||||
|
||||
### 步骤 3:配置容器端口
|
||||
|
||||
```bash
|
||||
# 让容器直接监听宿主机的 80/443/15021
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/ports","value":[
|
||||
{"containerPort":80,"hostPort":80,"protocol":"TCP","name":"http"},
|
||||
{"containerPort":443,"hostPort":443,"protocol":"TCP","name":"https"},
|
||||
{"containerPort":15021,"hostPort":15021,"protocol":"TCP","name":"status-port"},
|
||||
{"containerPort":15090,"protocol":"TCP","name":"http-envoy-prom"}]}]'
|
||||
```
|
||||
|
||||
### 步骤 4:配置安全上下文(解决权限问题)
|
||||
|
||||
```bash
|
||||
# 1. 添加 NET_BIND_SERVICE 能力
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/containers/0/securityContext/capabilities/add","value":["NET_BIND_SERVICE"]}]'
|
||||
|
||||
# 2. 以 root 身份运行(允许绑定特权端口)
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"replace","path":"/spec/template/spec/securityContext/runAsNonRoot","value":false},\
|
||||
{"op":"replace","path":"/spec/template/spec/securityContext/runAsUser","value":0},\
|
||||
{"op":"replace","path":"/spec/template/spec/securityContext/runAsGroup","value":0}]'
|
||||
|
||||
# 3. 设置环境变量(告知 Istio 这是特权 Pod)
|
||||
kubectl set env deployment/istio-ingressgateway -n istio-system ISTIO_META_UNPRIVILEGED_POD=false
|
||||
```
|
||||
|
||||
### 步骤 5:重启 Deployment
|
||||
|
||||
```bash
|
||||
# 先缩容到 0,避免 hostPort 冲突
|
||||
kubectl scale deployment istio-ingressgateway -n istio-system --replicas=0
|
||||
|
||||
# 等待 Pod 完全终止
|
||||
kubectl rollout status deployment/istio-ingressgateway -n istio-system --timeout=60s || true
|
||||
sleep 3
|
||||
|
||||
# 扩容到 1
|
||||
kubectl scale deployment istio-ingressgateway -n istio-system --replicas=1
|
||||
|
||||
# 等待新 Pod 就绪
|
||||
kubectl rollout status deployment/istio-ingressgateway -n istio-system --timeout=120s
|
||||
```
|
||||
|
||||
## 验证配置
|
||||
|
||||
### 1. 检查 Pod 状态
|
||||
|
||||
```bash
|
||||
# 查看 Pod 状态和 IP(hostNetwork 模式下 IP 应为节点 IP)
|
||||
kubectl get pods -n istio-system -o wide
|
||||
|
||||
# 确认 hostNetwork 已启用
|
||||
kubectl get pod -n istio-system -l app=istio-ingressgateway \
|
||||
-o jsonpath='{.items[0].spec.hostNetwork}'
|
||||
# 应该输出: true
|
||||
```
|
||||
|
||||
### 2. 检查端口监听
|
||||
|
||||
```bash
|
||||
# 在 master 节点上检查端口监听
|
||||
ss -tlnp | grep -E ':(80|443|15021) '
|
||||
|
||||
# 或在 Pod 内部检查
|
||||
kubectl exec -n istio-system deploy/istio-ingressgateway -- \
|
||||
ss -tlnp | grep -E ':(80|443|15021) '
|
||||
```
|
||||
|
||||
### 3. 检查 Istio 配置
|
||||
|
||||
```bash
|
||||
# 查看 Envoy listener 配置
|
||||
istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
|
||||
|
||||
# 检查配置分析
|
||||
istioctl analyze -A
|
||||
```
|
||||
|
||||
## 配置 Gateway 和 VirtualService
|
||||
|
||||
### 1. 准备 TLS 证书 Secret
|
||||
|
||||
如果证书 Secret 在其他命名空间,需要复制到 `istio-system`:
|
||||
|
||||
```bash
|
||||
# 复制 Secret(示例)
|
||||
kubectl get secret <your-tls-secret> -n <source-namespace> -o yaml | \
|
||||
sed "s/namespace: <source-namespace>/namespace: istio-system/" | \
|
||||
kubectl apply -f -
|
||||
|
||||
# 验证
|
||||
kubectl get secret <your-tls-secret> -n istio-system
|
||||
```
|
||||
|
||||
**注意**:证书文件(`.crt`)如果包含多个 `BEGIN CERTIFICATE` 块是正常的,这是证书链(服务器证书 + 中间证书)。Kubernetes Secret 和 Istio Gateway 都支持这种格式。
|
||||
|
||||
### 2. 创建 Gateway
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: devstar-gateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
selector:
|
||||
istio: ingressgateway
|
||||
servers:
|
||||
- port:
|
||||
number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
hosts:
|
||||
- devstar.cn
|
||||
- www.devstar.cn
|
||||
- port:
|
||||
number: 443
|
||||
name: https
|
||||
protocol: HTTPS
|
||||
tls:
|
||||
mode: SIMPLE
|
||||
credentialName: devstar-studio-tls-secret-devstar-cn
|
||||
hosts:
|
||||
- devstar.cn
|
||||
- www.devstar.cn
|
||||
```
|
||||
|
||||
### 3. 创建 VirtualService
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: devstar-studio-gitea
|
||||
namespace: devstar-studio-ns
|
||||
spec:
|
||||
hosts:
|
||||
- devstar.cn
|
||||
- www.devstar.cn
|
||||
gateways:
|
||||
- istio-system/devstar-gateway
|
||||
http:
|
||||
# www.devstar.cn 重定向到 devstar.cn (308 永久重定向)
|
||||
- match:
|
||||
- headers:
|
||||
host:
|
||||
exact: www.devstar.cn
|
||||
redirect:
|
||||
authority: devstar.cn
|
||||
redirectCode: 308
|
||||
# devstar.cn 路由到后端服务
|
||||
- match:
|
||||
- uri:
|
||||
prefix: /
|
||||
route:
|
||||
- destination:
|
||||
host: devstar-studio-gitea-http
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
### 4. 验证 Gateway 和 VirtualService
|
||||
|
||||
```bash
|
||||
# 检查 Gateway
|
||||
kubectl get gateway -n istio-system
|
||||
|
||||
# 检查 VirtualService
|
||||
kubectl get virtualservice -A
|
||||
|
||||
# 查看详细配置
|
||||
kubectl describe gateway devstar-gateway -n istio-system
|
||||
kubectl describe virtualservice devstar-studio-gitea -n devstar-studio-ns
|
||||
```
|
||||
|
||||
## 测试访问
|
||||
|
||||
```bash
|
||||
# HTTP 测试
|
||||
curl -H "Host: devstar.cn" http://<master-ip> -I
|
||||
|
||||
# HTTPS 测试
|
||||
curl -k --resolve devstar.cn:443:<master-ip> https://devstar.cn -I
|
||||
|
||||
# 测试重定向(www.devstar.cn -> devstar.cn)
|
||||
curl -I -H "Host: www.devstar.cn" http://<master-ip>
|
||||
# 应该返回: HTTP/1.1 308 Permanent Redirect
|
||||
```
|
||||
|
||||
## 启用服务网格(可选)
|
||||
|
||||
如果需要为其他命名空间启用自动 sidecar 注入:
|
||||
|
||||
```bash
|
||||
# 为命名空间启用自动注入
|
||||
kubectl label namespace <namespace> istio-injection=enabled
|
||||
|
||||
# 验证
|
||||
kubectl get namespace -L istio-injection
|
||||
|
||||
# 重启现有 Pod 以注入 sidecar
|
||||
kubectl rollout restart deployment -n <namespace>
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. Pod 一直 Pending
|
||||
|
||||
**原因**:旧 Pod 仍占用 hostPort,新 Pod 无法调度。
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 手动删除旧 Pod
|
||||
kubectl delete pod -n istio-system -l app=istio-ingressgateway
|
||||
|
||||
# 或先缩容再扩容
|
||||
kubectl scale deployment istio-ingressgateway -n istio-system --replicas=0
|
||||
kubectl scale deployment istio-ingressgateway -n istio-system --replicas=1
|
||||
```
|
||||
|
||||
### 2. Envoy 报 "Permission denied" 无法绑定 80/443
|
||||
|
||||
**原因**:容器没有足够权限绑定特权端口。
|
||||
|
||||
**解决**:
|
||||
- 确认已添加 `NET_BIND_SERVICE` capability
|
||||
- 确认 `runAsUser: 0` 和 `runAsNonRoot: false`
|
||||
- 确认 `ISTIO_META_UNPRIVILEGED_POD=false`
|
||||
|
||||
### 3. Istiod 日志显示 "skipping privileged gateway port"
|
||||
|
||||
**原因**:Istio 认为 Pod 是无特权模式。
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
kubectl set env deployment/istio-ingressgateway -n istio-system ISTIO_META_UNPRIVILEGED_POD=false
|
||||
kubectl rollout restart deployment istio-ingressgateway -n istio-system
|
||||
```
|
||||
|
||||
### 4. Gateway 冲突(IST0145)
|
||||
|
||||
**原因**:多个 Gateway 使用相同的 selector 和端口,但 hosts 冲突。
|
||||
|
||||
**解决**:
|
||||
- 合并多个 Gateway 到一个,在 `hosts` 中列出所有域名
|
||||
- 或确保不同 Gateway 的 `hosts` 不重叠
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果需要回滚到默认配置:
|
||||
|
||||
```bash
|
||||
# 1. 恢复 nginx(如果之前使用)
|
||||
kubectl scale deployment my-release-nginx-ingress-controller \
|
||||
-n nginx-ingress-controller --replicas=1
|
||||
|
||||
# 2. 恢复 Istio 为默认配置
|
||||
istioctl install --set profile=default -y
|
||||
|
||||
# 3. 或手动删除 hostNetwork 相关配置
|
||||
kubectl patch deployment istio-ingressgateway -n istio-system --type='json' \
|
||||
-p='[{"op":"remove","path":"/spec/template/spec/hostNetwork"}]'
|
||||
```
|
||||
|
||||
## 端口映射说明
|
||||
|
||||
### Istio 默认端口配置
|
||||
|
||||
- **容器内部端口**:Istio 默认让 Envoy 监听 8080(HTTP)和 8443(HTTPS)
|
||||
- **Service 端口映射**:Service 的 80 端口映射到容器的 8080(targetPort: 8080),443 映射到 8443
|
||||
- **为什么不是 80/443**:这是 Istio 的设计,避免与主机上的其他服务冲突
|
||||
|
||||
### hostNetwork 模式下的端口配置
|
||||
|
||||
使用 hostNetwork 模式时:
|
||||
- 容器直接使用主机网络,需要监听主机的 80/443 端口
|
||||
- 因此需要修改容器端口配置,让容器监听 80/443 而不是 8080/8443
|
||||
- 同时需要配置 IstioOperator 的 values,让 Envoy 实际监听 80/443
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **端口冲突**:迁移前确保停止 nginx 或其他占用 80/443 的服务
|
||||
2. **Sidecar 资源**:每个 Pod 会增加 ~100MB 内存和 ~100m CPU
|
||||
3. **TLS 证书**:需要将证书 Secret 复制到 istio-system 命名空间,或通过 Gateway 配置指定命名空间
|
||||
4. **性能影响**:sidecar 会增加少量延迟(通常 <1ms)
|
||||
5. **Service 类型**:hostNetwork 模式下,Service 类型可以是 `ClusterIP` 或 `LoadBalancer`,不影响功能
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 说明:
|
||||
# 在 master、node1、node2 三台节点上分别拉取指定镜像, 并导入到 containerd (k8s.io 命名空间)
|
||||
# 不通过主机分发镜像归档, 而是每台节点各自拉取/导入。
|
||||
#
|
||||
# 使用示例:
|
||||
# chmod +x k8s-image-pull-and-import.sh
|
||||
# ./k8s-image-pull-and-import.sh beppeb/devstar-controller-manager:3.0.0.without_istio
|
||||
#
|
||||
# 可选环境变量:
|
||||
# SSH_KEY 指定私钥路径 (默认: ~/.ssh/id_rsa, 若存在自动携带)
|
||||
|
||||
echo "==== K8s 镜像拉取并导入 containerd ===="
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "用法: $0 <IMAGE[:TAG]>"
|
||||
echo "示例: $0 beppeb/devstar-controller-manager:3.0.0.without_istio"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_INPUT="$1"
|
||||
|
||||
# 规范化镜像名, 若无 registry 前缀则补全 docker.io/
|
||||
normalize_image() {
|
||||
local img="$1"
|
||||
if [[ "$img" != */*/* ]]; then
|
||||
# 只有一个斜杠(如 library/nginx 或 beppeb/devstar-...): 仍可能缺少 registry
|
||||
# Docker 的默认 registry 是 docker.io
|
||||
echo "docker.io/${img}"
|
||||
else
|
||||
echo "$img"
|
||||
fi
|
||||
}
|
||||
|
||||
CANONICAL_IMAGE=$(normalize_image "$IMAGE_INPUT")
|
||||
echo "目标镜像: ${CANONICAL_IMAGE}"
|
||||
|
||||
# 节点列表: 与 k8s-step1-prepare-env.sh 风格一致
|
||||
NODES=("172.17.0.15:master" "172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 本机 IP 与 SSH 选项
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
|
||||
run_remote() {
|
||||
local ip="$1"; shift
|
||||
local cmd="$*"
|
||||
if [ "$ip" = "$LOCAL_IP" ]; then
|
||||
bash -lc "$cmd"
|
||||
else
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@"$ip" "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
# 在远端节点执行: 使用 docker 或 containerd 拉取镜像, 并确保导入到 containerd k8s.io
|
||||
remote_pull_and_import_cmd() {
|
||||
local image="$1"
|
||||
# 注意: 使用单引号包裹, 传到远端后再展开变量
|
||||
cat <<'EOF_REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_REMOTE="$IMAGE_PLACEHOLDER"
|
||||
|
||||
has_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
echo "[\"$(hostname)\"] 处理镜像: ${IMAGE_REMOTE}"
|
||||
|
||||
# 优先尝试 docker 拉取, 成功后直接导入 containerd (无需落盘)
|
||||
if has_cmd docker; then
|
||||
echo "[\"$(hostname)\"] 使用 docker pull"
|
||||
sudo docker pull "${IMAGE_REMOTE}"
|
||||
echo "[\"$(hostname)\"] 导入到 containerd (k8s.io)"
|
||||
sudo docker save "${IMAGE_REMOTE}" | sudo ctr -n k8s.io images import - >/dev/null
|
||||
else
|
||||
echo "[\"$(hostname)\"] 未检测到 docker, 尝试使用 containerd 拉取"
|
||||
# containerd 直接拉取到 k8s.io 命名空间
|
||||
sudo ctr -n k8s.io images pull --all-platforms "${IMAGE_REMOTE}"
|
||||
fi
|
||||
|
||||
# 规范化 tag: 若镜像缺少 docker.io 前缀, 在 containerd 内补齐一份别名
|
||||
NEED_PREFIX=0
|
||||
if [[ "${IMAGE_REMOTE}" != docker.io/* ]]; then
|
||||
NEED_PREFIX=1
|
||||
fi
|
||||
|
||||
if [ "$NEED_PREFIX" -eq 1 ]; then
|
||||
# 仅当不存在 docker.io/ 前缀时, 补一个 docker.io/ 的 tag, 方便与清单匹配
|
||||
# 计算补齐后的名字
|
||||
if [[ "${IMAGE_REMOTE}" == */*/* ]]; then
|
||||
# 已有显式 registry, 不重复打 tag
|
||||
:
|
||||
else
|
||||
FIXED="docker.io/${IMAGE_REMOTE}"
|
||||
echo "[\"$(hostname)\"] 为 containerd 打标签: ${FIXED}"
|
||||
sudo ctr -n k8s.io images tag "${IMAGE_REMOTE}" "${FIXED}" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[\"$(hostname)\"] 验证镜像是否存在于 containerd:"
|
||||
sudo ctr -n k8s.io images ls | grep -E "$(printf '%s' "${IMAGE_REMOTE}" | sed 's/[\/.\-]/\\&/g')" || true
|
||||
EOF_REMOTE
|
||||
}
|
||||
|
||||
# 遍历节点执行
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "==== 在 ${hostname} (${ip}) 执行镜像拉取与导入 ===="
|
||||
# 将占位符替换为实际镜像并远程执行
|
||||
remote_script=$(remote_pull_and_import_cmd "$CANONICAL_IMAGE")
|
||||
# 安全替换占位符为镜像名
|
||||
remote_script=${remote_script//\$IMAGE_PLACEHOLDER/$CANONICAL_IMAGE}
|
||||
run_remote "$ip" "$remote_script"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "==== 完成 ===="
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 集群一键安装脚本
|
||||
# 功能: 按顺序执行所有安装步骤
|
||||
|
||||
echo "==== Kubernetes 集群一键安装 ===="
|
||||
echo "集群信息:"
|
||||
echo "- Master: 172.17.0.15"
|
||||
echo "- Node1: 172.17.0.43"
|
||||
echo "- Node2: 172.17.0.34"
|
||||
echo "- Kubernetes 版本: v1.32.3"
|
||||
echo "- 网络插件: Flannel"
|
||||
echo "- 容器运行时: containerd"
|
||||
echo ""
|
||||
|
||||
# 检查脚本文件是否存在
|
||||
SCRIPTS=(
|
||||
"k8s-step1-prepare-env.sh"
|
||||
"k8s-step2-install-containerd.sh"
|
||||
"k8s-step3-install-components.sh"
|
||||
"k8s-step4-init-cluster.sh"
|
||||
"k8s-step5-install-flannel.sh"
|
||||
"k8s-step6-join-nodes.sh"
|
||||
)
|
||||
|
||||
for script in "${SCRIPTS[@]}"; do
|
||||
if [ ! -f "$script" ]; then
|
||||
echo "错误: 找不到脚本文件 $script"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "所有脚本文件检查完成,开始安装..."
|
||||
echo ""
|
||||
|
||||
# 执行安装步骤
|
||||
echo "==== 步骤 1: 环境准备 ===="
|
||||
./k8s-step1-prepare-env.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 步骤 2: 安装 containerd ===="
|
||||
./k8s-step2-install-containerd.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 步骤 3: 安装 Kubernetes 组件 ===="
|
||||
./k8s-step3-install-components.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 步骤 4: 初始化集群 ===="
|
||||
./k8s-step4-init-cluster.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 步骤 5: 安装 Flannel 网络插件 ===="
|
||||
./k8s-step5-install-flannel.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 步骤 6: 节点加入集群 ===="
|
||||
./k8s-step6-join-nodes.sh
|
||||
echo ""
|
||||
|
||||
echo "==== 安装完成 ===="
|
||||
echo "集群状态:"
|
||||
kubectl get nodes
|
||||
echo ""
|
||||
kubectl get pods -A
|
||||
echo ""
|
||||
echo "集群已就绪,可以开始部署应用!"
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
# Kubernetes 集群安装文档
|
||||
|
||||
## 📋 集群信息
|
||||
- **Master**: 172.17.0.15 (master)
|
||||
- **Node1**: 172.17.0.43 (node1)
|
||||
- **Node2**: 172.17.0.34 (node2)
|
||||
- **Kubernetes 版本**: v1.32.3
|
||||
- **容器运行时**: containerd
|
||||
- **网络插件**: Flannel
|
||||
- **镜像仓库**: 阿里云镜像
|
||||
|
||||
## 🎯 安装方式
|
||||
**模块化安装**: 每个脚本功能清晰,可以单独执行或按顺序执行
|
||||
|
||||
## 📋 安装脚本
|
||||
|
||||
### 🔧 脚本列表
|
||||
1. **`k8s-step1-prepare-env.sh`** - 环境准备 (所有节点)
|
||||
2. **`k8s-step2-install-containerd.sh`** - 容器运行时安装 (所有节点)
|
||||
3. **`k8s-step3-install-components.sh`** - Kubernetes 组件安装 (所有节点)
|
||||
4. **`k8s-step4-init-cluster.sh`** - 集群初始化 (Master 节点)
|
||||
5. **`k8s-step5-install-flannel.sh`** - 网络插件安装 (Master 节点)
|
||||
6. **`k8s-step6-join-nodes.sh`** - 节点加入集群 (Node1, Node2)
|
||||
7. **`k8s-install-all.sh`** - 主控制脚本 (按顺序执行所有步骤)
|
||||
|
||||
### 🌐 网络配置脚本
|
||||
- **`setup-master-gateway.sh`** - Master 节点网关配置
|
||||
- **`setup-node1.sh`** - Node1 网络路由配置
|
||||
- **`setup-node2.sh`** - Node2 网络路由配置
|
||||
|
||||
### 🔧 辅助工具脚本
|
||||
- **`install-kubectl-nodes.sh`** - 为其他节点安装 kubectl
|
||||
|
||||
### 🚀 使用方法
|
||||
|
||||
#### 方法 1: 一键安装
|
||||
```bash
|
||||
# 在 Master 节点运行
|
||||
./k8s-install-all.sh
|
||||
```
|
||||
|
||||
#### 方法 2: 分步安装
|
||||
```bash
|
||||
# 按顺序执行每个步骤
|
||||
./k8s-step1-prepare-env.sh
|
||||
./k8s-step2-install-containerd.sh
|
||||
./k8s-step3-install-components.sh
|
||||
./k8s-step4-init-cluster.sh
|
||||
./k8s-step5-install-flannel.sh
|
||||
./k8s-step6-join-nodes.sh
|
||||
|
||||
# 可选:为其他节点安装 kubectl
|
||||
./install-kubectl-nodes.sh
|
||||
```
|
||||
|
||||
## 📋 安装步骤
|
||||
|
||||
### ✅ 步骤 1: 环境准备(已完成)
|
||||
- [x] 云主机重装系统:确认系统盘数据清空,无残留 kube 目录与服务
|
||||
- [x] 主机名设置:`master`、`node1`、`node2`(不在节点脚本中写入 hosts)
|
||||
- [x] Master 配置 NAT 网关:开启 `net.ipv4.ip_forward`,设置 `iptables` MASQUERADE 并持久化
|
||||
- [x] 基础内核与网络:开启 `overlay`、`br_netfilter`;`sysctl` 应用桥接与转发参数
|
||||
- [x] 关闭 swap:禁用并注释 `/etc/fstab` 对应项
|
||||
- [x] 防火墙:禁用 `ufw`,确保必要端口不被拦截
|
||||
- [x] SSH 信任:在 master 生成密钥并分发到 `node1/node2`,验证免密可达
|
||||
|
||||
### ✅ 步骤 2: 容器运行时准备(所有节点,已完成)
|
||||
- [x] 更新系统包,安装依赖工具:`curl`、`wget`、`gnupg`、`ca-certificates`、`apt-transport-https` 等
|
||||
- [x] 安装 containerd 并生成默认配置 `/etc/containerd/config.toml`
|
||||
- [x] 配置镜像加速:docker.io/quay.io 使用腾讯云镜像,其他使用高校镜像
|
||||
- [x] 安装 CNI 插件 v1.3.0(在 master 预下载并分发至 node1/node2)
|
||||
- [x] 启用并开机自启 `containerd`,确认服务状态正常
|
||||
|
||||
### ✅ 步骤 3: 安装 Kubernetes 组件(所有节点,已完成)
|
||||
- [x] 添加 Kubernetes APT 仓库(pkgs.k8s.io v1.32),修复 GPG key 与源配置问题
|
||||
- [x] 安装并锁定版本:`kubelet`、`kubeadm`、`kubectl` 为 `v1.32.3`
|
||||
- [x] 配置 kubelet:使用 `systemd` cgroup,与 containerd 对齐,写入完整配置文件
|
||||
- [x] 启用并启动 `kubelet` 服务
|
||||
|
||||
### ✅ 步骤 4: 集群初始化(Master 节点,已完成)
|
||||
- [x] 执行 `kubeadm init` 完成初始化:包含 `controlPlaneEndpoint=172.17.0.15:6443`、Networking(ServiceCIDR `10.96.0.0/12`、PodCIDR `10.244.0.0/16`)、`imageRepository`(Aliyun)
|
||||
- [x] 拷贝 `admin.conf` 到 `~/.kube/config` 并验证控制面组件:`etcd`、`kube-apiserver`、`kube-controller-manager`、`kube-scheduler`、`kube-proxy` 均 Running;`coredns` Pending(等待安装网络插件)
|
||||
- [x] 生成并使用 `kubeadm token create --print-join-command` 生成 join 命令
|
||||
|
||||
### ✅ 步骤 5: 网络插件安装 (Master 节点,已完成)
|
||||
- [x] 下载并应用 Flannel v0.27.4 清单
|
||||
- [x] 匹配 Pod CIDR `10.244.0.0/16`,等待组件 Ready
|
||||
- [x] 配置 Flannel 使用国内镜像源(registry-k8s-io.mirrors.sjtug.sjtu.edu.cn、ghcr.tencentcloudcr.com)
|
||||
- [x] 预拉取所有 Flannel 镜像并打标签
|
||||
- [x] 等待所有网络组件就绪:kube-flannel-ds、coredns
|
||||
|
||||
### ✅ 步骤 6: 节点加入集群(已完成)
|
||||
- [x] 读取 `node-join-command.txt` 文件中的 join 命令
|
||||
- [x] 在 `node1/node2` 执行 join,加入成功后验证 `Ready`
|
||||
- [x] 验证所有节点状态:master (Ready, control-plane)、node1 (Ready)、node2 (Ready)
|
||||
|
||||
### ✅ 步骤 7: 集群验证(已完成)
|
||||
- [x] `kubectl get nodes/pods -A` 基线检查
|
||||
- [x] 所有 Pod 状态为 Running:控制面组件、网络组件、系统组件
|
||||
- [x] 集群完全就绪,可以部署应用
|
||||
|
||||
### ✅ 步骤 8: 为其他节点安装 kubectl(已完成)
|
||||
- [x] 在 node1 和 node2 上安装 kubectl v1.32.3
|
||||
- [x] 复制 master 的 kubeconfig 配置文件到其他节点
|
||||
- [x] 验证所有节点都能正常访问 Kubernetes 集群
|
||||
|
||||
## 📝 详细安装过程记录
|
||||
|
||||
### 步骤 1: 系统环境准备
|
||||
|
||||
#### 1.1 系统重装与清理
|
||||
- 腾讯云服务器实例重装系统,确保硬盘完全清空
|
||||
- 验证无残留 Kubernetes 相关目录和服务
|
||||
|
||||
#### 1.2 主机名配置
|
||||
```bash
|
||||
# Master 节点
|
||||
sudo hostnamectl set-hostname master
|
||||
|
||||
# Node1 节点
|
||||
sudo hostnamectl set-hostname node1
|
||||
|
||||
# Node2 节点
|
||||
sudo hostnamectl set-hostname node2
|
||||
```
|
||||
|
||||
#### 1.3 网络配置
|
||||
|
||||
> **提示**: 可以使用提供的脚本自动配置网络:
|
||||
> - `./setup-master-gateway.sh` - 在 Master 节点执行
|
||||
> - `./setup-node1.sh` - 在 Node1 节点执行
|
||||
> - `./setup-node2.sh` - 在 Node2 节点执行
|
||||
|
||||
**Master 节点配置为 NAT 网关:**
|
||||
```bash
|
||||
# 启用 IP 转发
|
||||
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
|
||||
# 清空现有 iptables 规则
|
||||
sudo iptables -F
|
||||
sudo iptables -t nat -F
|
||||
sudo iptables -t mangle -F
|
||||
sudo iptables -X
|
||||
sudo iptables -t nat -X
|
||||
sudo iptables -t mangle -X
|
||||
|
||||
# 设置默认策略
|
||||
sudo iptables -P INPUT ACCEPT
|
||||
sudo iptables -P FORWARD ACCEPT
|
||||
sudo iptables -P OUTPUT ACCEPT
|
||||
|
||||
# 配置 NAT 规则 - 允许内网节点通过 master 访问外网
|
||||
sudo iptables -t nat -A POSTROUTING -s 172.17.0.0/20 -o eth0 -j MASQUERADE
|
||||
|
||||
# 允许转发来自内网的流量
|
||||
sudo iptables -A FORWARD -s 172.17.0.0/20 -j ACCEPT
|
||||
sudo iptables -A FORWARD -d 172.17.0.0/20 -j ACCEPT
|
||||
|
||||
# 保存 iptables 规则
|
||||
sudo apt update && sudo apt install -y iptables-persistent
|
||||
sudo netfilter-persistent save
|
||||
```
|
||||
|
||||
**Node1 和 Node2 配置路由:**
|
||||
```bash
|
||||
# 删除默认网关(如果存在)
|
||||
sudo ip route del default 2>/dev/null || true
|
||||
|
||||
# 添加默认网关指向 master
|
||||
sudo ip route add default via 172.17.0.15
|
||||
|
||||
# 验证网络连通性
|
||||
ping -c 2 172.17.0.15 && echo "✓ 可以访问 master" || echo "✗ 无法访问 master"
|
||||
ping -c 2 8.8.8.8 && echo "✓ 可以访问外网" || echo "✗ 无法访问外网"
|
||||
```
|
||||
|
||||
#### 1.4 SSH 密钥配置
|
||||
```bash
|
||||
# Master 节点生成 SSH 密钥
|
||||
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
|
||||
|
||||
# 将公钥复制到 Node1 和 Node2
|
||||
ssh-copy-id ubuntu@172.17.0.43
|
||||
ssh-copy-id ubuntu@172.17.0.34
|
||||
```
|
||||
|
||||
### 步骤 2: 基础环境准备(所有节点)
|
||||
|
||||
#### 2.1 系统更新
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y curl wget vim net-tools gnupg lsb-release ca-certificates apt-transport-https
|
||||
```
|
||||
|
||||
#### 2.2 内核参数配置
|
||||
```bash
|
||||
# 加载内核模块
|
||||
sudo modprobe overlay
|
||||
sudo modprobe br_netfilter
|
||||
|
||||
# 配置内核参数
|
||||
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
|
||||
overlay
|
||||
br_netfilter
|
||||
EOF
|
||||
|
||||
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
|
||||
net.bridge.bridge-nf-call-iptables = 1
|
||||
net.bridge.bridge-nf-call-ip6tables = 1
|
||||
net.ipv4.ip_forward = 1
|
||||
EOF
|
||||
|
||||
sudo sysctl --system
|
||||
```
|
||||
|
||||
#### 2.3 禁用 Swap
|
||||
```bash
|
||||
sudo swapoff -a
|
||||
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
|
||||
```
|
||||
|
||||
#### 2.4 防火墙配置
|
||||
```bash
|
||||
sudo ufw disable
|
||||
```
|
||||
|
||||
### 步骤 3: 容器运行时安装(所有节点)
|
||||
|
||||
#### 3.1 安装 containerd
|
||||
```bash
|
||||
# 安装 containerd
|
||||
sudo apt update
|
||||
sudo apt install -y containerd
|
||||
|
||||
# ① 停止 containerd
|
||||
sudo systemctl stop containerd
|
||||
|
||||
# ② 生成默认配置
|
||||
sudo containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
|
||||
|
||||
# ③ 注入镜像加速配置(docker.io/quay.io:腾讯云,其它:高校镜像优先)
|
||||
sudo sed -i '/\[plugins."io.containerd.grpc.v1.cri".registry.mirrors\]/a\
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]\n endpoint = ["https://mirror.ccs.tencentyun.com"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]\n endpoint = ["https://quay.tencentcloudcr.com"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]\n endpoint = ["https://ghcr.nju.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]\n endpoint = ["https://gcr.nju.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]\n endpoint = ["https://registry-k8s-io.mirrors.sjtug.sjtu.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]\n endpoint = ["https://gcr.nju.edu.cn"]' /etc/containerd/config.toml
|
||||
|
||||
# ④ 重新加载并启动 containerd
|
||||
sudo systemctl daemon-reexec
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart containerd
|
||||
|
||||
# ⑤ 检查服务状态
|
||||
sudo systemctl status containerd --no-pager -l
|
||||
```
|
||||
|
||||
#### 3.2 安装 CNI 插件
|
||||
```bash
|
||||
# 下载 CNI 插件
|
||||
CNI_VERSION="v1.3.0"
|
||||
CNI_TGZ="cni-plugins-linux-amd64-${CNI_VERSION}.tgz"
|
||||
|
||||
# 下载 CNI 插件
|
||||
curl -L --fail --retry 3 --connect-timeout 10 \
|
||||
-o "$CNI_TGZ" \
|
||||
"https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/$CNI_TGZ"
|
||||
|
||||
# 安装 CNI 插件
|
||||
sudo mkdir -p /opt/cni/bin
|
||||
sudo tar -xzf "$CNI_TGZ" -C /opt/cni/bin/
|
||||
rm -f "$CNI_TGZ"
|
||||
```
|
||||
|
||||
### 步骤 4: Kubernetes 组件安装(所有节点)
|
||||
|
||||
#### 4.1 添加 Kubernetes 仓库
|
||||
```bash
|
||||
# 添加 Kubernetes 仓库 (pkgs.k8s.io v1.32)
|
||||
# 确保 keyrings 目录存在并可读
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list >/dev/null
|
||||
|
||||
# 更新包列表
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
#### 4.2 安装 Kubernetes 组件
|
||||
```bash
|
||||
# 安装 kubelet, kubeadm, kubectl
|
||||
sudo apt install -y kubelet kubeadm kubectl
|
||||
|
||||
# 锁定版本防止自动更新
|
||||
sudo apt-mark hold kubelet kubeadm kubectl
|
||||
```
|
||||
|
||||
#### 4.3 配置 kubelet
|
||||
```bash
|
||||
# 配置 kubelet
|
||||
sudo mkdir -p /var/lib/kubelet
|
||||
cat <<EOF | sudo tee /var/lib/kubelet/config.yaml
|
||||
apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
authentication:
|
||||
anonymous:
|
||||
enabled: false
|
||||
webhook:
|
||||
enabled: true
|
||||
x509:
|
||||
clientCAFile: /etc/kubernetes/pki/ca.crt
|
||||
authorization:
|
||||
mode: Webhook
|
||||
clusterDomain: cluster.local
|
||||
clusterDNS:
|
||||
- 10.96.0.10
|
||||
containerRuntimeEndpoint: unix:///var/run/containerd/containerd.sock
|
||||
cgroupDriver: systemd
|
||||
failSwapOn: false
|
||||
hairpinMode: promiscuous-bridge
|
||||
healthzBindAddress: 127.0.0.1
|
||||
healthzPort: 10248
|
||||
httpCheckFrequency: 20s
|
||||
imageMinimumGCAge: 2m0s
|
||||
imageGCHighThresholdPercent: 85
|
||||
imageGCLowThresholdPercent: 80
|
||||
iptablesDropBit: 15
|
||||
iptablesMasqueradeBit: 15
|
||||
kubeAPIBurst: 10
|
||||
kubeAPIQPS: 5
|
||||
makeIPTablesUtilChains: true
|
||||
maxOpenFiles: 1000000
|
||||
maxPods: 110
|
||||
nodeStatusUpdateFrequency: 10s
|
||||
oomScoreAdj: -999
|
||||
podCIDR: 10.244.0.0/16
|
||||
registryBurst: 10
|
||||
registryPullQPS: 5
|
||||
resolvConf: /etc/resolv.conf
|
||||
rotateCertificates: true
|
||||
runtimeRequestTimeout: 2m0s
|
||||
serializeImagePulls: true
|
||||
serverTLSBootstrap: true
|
||||
streamingConnectionIdleTimeout: 4h0m0s
|
||||
syncFrequency: 1m0s
|
||||
volumeStatsAggPeriod: 1m0s
|
||||
EOF
|
||||
|
||||
# 启动 kubelet
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable kubelet
|
||||
sudo systemctl start kubelet
|
||||
```
|
||||
|
||||
### 步骤 5: 集群初始化(Master 节点)
|
||||
|
||||
#### 5.1 初始化集群
|
||||
```bash
|
||||
# 初始化 Kubernetes 集群
|
||||
sudo kubeadm init \
|
||||
--apiserver-advertise-address=172.17.0.15 \
|
||||
--control-plane-endpoint=172.17.0.15:6443 \
|
||||
--kubernetes-version=v1.32.3 \
|
||||
--service-cidr=10.96.0.0/12 \
|
||||
--pod-network-cidr=10.244.0.0/16 \
|
||||
--image-repository=registry.aliyuncs.com/google_containers \
|
||||
--upload-certs \
|
||||
--ignore-preflight-errors=Swap
|
||||
|
||||
# 配置 kubectl
|
||||
mkdir -p $HOME/.kube
|
||||
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
|
||||
sudo chown $(id -u):$(id -g) $HOME/.kube/config
|
||||
```
|
||||
|
||||
#### 5.2 生成节点加入命令
|
||||
```bash
|
||||
# 生成节点加入命令
|
||||
JOIN_COMMAND=$(kubeadm token create --print-join-command)
|
||||
echo "节点加入命令:"
|
||||
echo "$JOIN_COMMAND"
|
||||
echo "$JOIN_COMMAND" > node-join-command.txt
|
||||
```
|
||||
|
||||
### 步骤 6: 网络插件安装(Master 节点)
|
||||
|
||||
#### 6.1 下载 Flannel 清单
|
||||
```bash
|
||||
# 下载 Flannel v0.27.4
|
||||
FLANNEL_VER="v0.27.4"
|
||||
curl -fsSL https://raw.githubusercontent.com/flannel-io/flannel/${FLANNEL_VER}/Documentation/kube-flannel.yml -O
|
||||
|
||||
# 修改 Flannel 配置
|
||||
sed -i 's|"Network": "10.244.0.0/16"|"Network": "10.244.0.0/16"|g' kube-flannel.yml
|
||||
```
|
||||
|
||||
#### 6.2 预拉取 Flannel 镜像
|
||||
```bash
|
||||
# 预拉取并打标签
|
||||
REGISTRY_K8S_MIRROR="registry-k8s-io.mirrors.sjtug.sjtu.edu.cn"
|
||||
GHCR_MIRROR="ghcr.tencentcloudcr.com"
|
||||
|
||||
# 预拉取 pause 镜像
|
||||
sudo ctr -n k8s.io images pull ${REGISTRY_K8S_MIRROR}/pause:3.8 || true
|
||||
sudo ctr -n k8s.io images tag ${REGISTRY_K8S_MIRROR}/pause:3.8 registry.k8s.io/pause:3.8 || true
|
||||
|
||||
# 预拉取 flannel 镜像
|
||||
sudo ctr -n k8s.io images pull ${GHCR_MIRROR}/flannel-io/flannel:${FLANNEL_VER} || true
|
||||
sudo ctr -n k8s.io images tag ${GHCR_MIRROR}/flannel-io/flannel:${FLANNEL_VER} ghcr.io/flannel-io/flannel:${FLANNEL_VER} || true
|
||||
```
|
||||
|
||||
#### 6.3 安装 Flannel
|
||||
```bash
|
||||
# 安装 Flannel
|
||||
kubectl apply -f kube-flannel.yml
|
||||
|
||||
# 等待 Flannel 组件就绪
|
||||
kubectl -n kube-flannel rollout status daemonset/kube-flannel-ds --timeout=600s
|
||||
kubectl wait --for=condition=ready pod -l app=flannel -n kube-flannel --timeout=600s
|
||||
|
||||
# 等待 CoreDNS 就绪
|
||||
kubectl -n kube-system rollout status deploy/coredns --timeout=600s
|
||||
```
|
||||
|
||||
### 步骤 7: 节点加入集群
|
||||
|
||||
#### 7.1 节点加入
|
||||
```bash
|
||||
# 检查是否存在加入命令文件
|
||||
if [ ! -f "node-join-command.txt" ]; then
|
||||
echo "错误: 找不到 node-join-command.txt 文件"
|
||||
echo "请先运行 k8s-step4-init-cluster.sh 初始化集群"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 读取加入命令
|
||||
JOIN_COMMAND=$(cat node-join-command.txt)
|
||||
echo "使用加入命令: $JOIN_COMMAND"
|
||||
|
||||
# Node1 加入集群
|
||||
ssh ubuntu@172.17.0.43 "sudo $JOIN_COMMAND"
|
||||
|
||||
# Node2 加入集群
|
||||
ssh ubuntu@172.17.0.34 "sudo $JOIN_COMMAND"
|
||||
|
||||
# 等待节点加入
|
||||
sleep 30
|
||||
|
||||
# 验证集群状态
|
||||
kubectl get nodes
|
||||
kubectl get pods -n kube-system
|
||||
kubectl get pods -n kube-flannel
|
||||
```
|
||||
|
||||
### 步骤 8: 集群验证
|
||||
|
||||
#### 8.1 验证节点状态
|
||||
```bash
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
#### 8.2 验证 Pod 状态
|
||||
```bash
|
||||
kubectl get pods -A
|
||||
```
|
||||
|
||||
#### 8.3 验证集群功能
|
||||
```bash
|
||||
# 检查集群信息
|
||||
kubectl cluster-info
|
||||
|
||||
# 检查节点详细信息
|
||||
kubectl describe nodes
|
||||
```
|
||||
|
||||
### 步骤 9: 为其他节点安装 kubectl
|
||||
|
||||
#### 9.1 在 node1 和 node2 安装 kubectl
|
||||
```bash
|
||||
# 检查是否已安装
|
||||
if command -v kubectl &> /dev/null; then
|
||||
echo "kubectl 已安装,版本: $(kubectl version --client 2>/dev/null | grep 'Client Version' || echo 'unknown')"
|
||||
echo "跳过安装"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 安装 kubectl
|
||||
sudo apt update
|
||||
sudo apt install -y apt-transport-https ca-certificates curl
|
||||
|
||||
# 添加 Kubernetes 官方 GPG key
|
||||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
|
||||
# 添加 Kubernetes apt 仓库
|
||||
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
|
||||
|
||||
# 更新包列表并安装 kubectl
|
||||
sudo apt update
|
||||
sudo apt install -y kubectl
|
||||
```
|
||||
|
||||
#### 9.2 复制 kubeconfig 配置文件
|
||||
```bash
|
||||
# 在 master 节点执行
|
||||
# 为 node1 创建 .kube 目录
|
||||
ssh ubuntu@172.17.0.43 "mkdir -p ~/.kube"
|
||||
|
||||
# 为 node2 创建 .kube 目录
|
||||
ssh ubuntu@172.17.0.34 "mkdir -p ~/.kube"
|
||||
|
||||
# 复制 kubeconfig 到 node1
|
||||
scp ~/.kube/config ubuntu@172.17.0.43:~/.kube/config
|
||||
|
||||
# 复制 kubeconfig 到 node2
|
||||
scp ~/.kube/config ubuntu@172.17.0.34:~/.kube/config
|
||||
```
|
||||
|
||||
#### 9.3 验证 kubectl 连接
|
||||
```bash
|
||||
# 验证 node1 kubectl 连接
|
||||
ssh ubuntu@172.17.0.43 "kubectl get nodes"
|
||||
|
||||
# 验证 node2 kubectl 连接
|
||||
ssh ubuntu@172.17.0.34 "kubectl get nodes"
|
||||
```
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== Kubernetes 环境准备 ===="
|
||||
|
||||
# 1. 更新系统包
|
||||
echo "更新系统包..."
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 2. 安装必要的工具
|
||||
echo "安装必要工具..."
|
||||
sudo apt install -y curl wget gnupg lsb-release ca-certificates apt-transport-https software-properties-common
|
||||
|
||||
# 3. 禁用 swap
|
||||
echo "禁用 swap..."
|
||||
sudo swapoff -a
|
||||
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
|
||||
|
||||
# 4. 配置内核参数
|
||||
echo "配置内核参数..."
|
||||
cat <<EOF_MODULES | sudo tee /etc/modules-load.d/k8s.conf
|
||||
overlay
|
||||
br_netfilter
|
||||
EOF_MODULES
|
||||
|
||||
sudo modprobe overlay
|
||||
sudo modprobe br_netfilter
|
||||
|
||||
# 5. 配置 sysctl 参数
|
||||
echo "配置 sysctl 参数..."
|
||||
cat <<EOF_SYSCTL | sudo tee /etc/sysctl.d/k8s.conf
|
||||
net.bridge.bridge-nf-call-iptables = 1
|
||||
net.bridge.bridge-nf-call-ip6tables = 1
|
||||
net.ipv4.ip_forward = 1
|
||||
EOF_SYSCTL
|
||||
|
||||
sudo sysctl --system
|
||||
|
||||
# 6. 配置防火墙
|
||||
echo "配置防火墙..."
|
||||
sudo ufw --force disable || true
|
||||
|
||||
# 按你的要求,不在节点上修改 /etc/hosts
|
||||
|
||||
echo "==== 环境准备完成 ===="
|
||||
echo "当前主机名: $(hostname)"
|
||||
echo "当前 IP: $(ip route get 1 | awk '{print $7; exit}')"
|
||||
echo "Swap 状态: $(swapon --show | wc -l) 个 swap 分区"
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 环境准备脚本
|
||||
# 功能: 在所有节点准备 Kubernetes 运行环境
|
||||
|
||||
echo "==== Kubernetes 环境准备 ===="
|
||||
|
||||
# 定义节点列表
|
||||
NODES=("172.17.0.15:master" "172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 本机 IP 与 SSH 选项
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
# SSH 私钥(可用环境变量 SSH_KEY 覆盖),存在则自动携带
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
|
||||
# 函数:在所有节点执行命令
|
||||
execute_on_all_nodes() {
|
||||
local command="$1"
|
||||
local description="$2"
|
||||
|
||||
echo "==== $description ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "在 $hostname ($ip) 执行: $command"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
bash -lc "$command"
|
||||
else
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@$ip "$command"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函数:传输文件到所有节点
|
||||
copy_to_all_nodes() {
|
||||
local file="$1"
|
||||
echo "==== 传输文件 $file 到所有节点 ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "传输到 $hostname ($ip)"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
cp -f "$file" ~/
|
||||
else
|
||||
scp $SSH_OPTS $SSH_ID "$file" ubuntu@$ip:~/
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 创建环境准备脚本
|
||||
cat > k8s-prepare-env.sh << 'EOF_OUTER'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== Kubernetes 环境准备 ===="
|
||||
|
||||
# 1. 更新系统包
|
||||
echo "更新系统包..."
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 2. 安装必要的工具
|
||||
echo "安装必要工具..."
|
||||
sudo apt install -y curl wget gnupg lsb-release ca-certificates apt-transport-https software-properties-common
|
||||
|
||||
# 3. 禁用 swap
|
||||
echo "禁用 swap..."
|
||||
sudo swapoff -a
|
||||
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
|
||||
|
||||
# 4. 配置内核参数
|
||||
echo "配置内核参数..."
|
||||
cat <<EOF_MODULES | sudo tee /etc/modules-load.d/k8s.conf
|
||||
overlay
|
||||
br_netfilter
|
||||
EOF_MODULES
|
||||
|
||||
sudo modprobe overlay
|
||||
sudo modprobe br_netfilter
|
||||
|
||||
# 5. 配置 sysctl 参数
|
||||
echo "配置 sysctl 参数..."
|
||||
cat <<EOF_SYSCTL | sudo tee /etc/sysctl.d/k8s.conf
|
||||
net.bridge.bridge-nf-call-iptables = 1
|
||||
net.bridge.bridge-nf-call-ip6tables = 1
|
||||
net.ipv4.ip_forward = 1
|
||||
EOF_SYSCTL
|
||||
|
||||
sudo sysctl --system
|
||||
|
||||
# 6. 配置防火墙
|
||||
echo "配置防火墙..."
|
||||
sudo ufw --force disable || true
|
||||
|
||||
# 按你的要求,不在节点上修改 /etc/hosts
|
||||
|
||||
echo "==== 环境准备完成 ===="
|
||||
echo "当前主机名: $(hostname)"
|
||||
echo "当前 IP: $(ip route get 1 | awk '{print $7; exit}')"
|
||||
echo "Swap 状态: $(swapon --show | wc -l) 个 swap 分区"
|
||||
EOF_OUTER
|
||||
|
||||
chmod +x k8s-prepare-env.sh
|
||||
copy_to_all_nodes k8s-prepare-env.sh
|
||||
execute_on_all_nodes "./k8s-prepare-env.sh" "环境准备"
|
||||
|
||||
echo "==== 环境准备完成 ===="
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 容器运行时安装脚本
|
||||
# 功能: 在所有节点安装 containerd 和 CNI 插件
|
||||
|
||||
echo "==== 安装容器运行时 (containerd) ===="
|
||||
|
||||
# 定义节点列表
|
||||
NODES=("172.17.0.15:master" "172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 本机 IP 与 SSH 选项
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
# SSH 私钥(可用环境变量 SSH_KEY 覆盖),存在则自动携带
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
|
||||
# 统一的工件目录与文件名(在 master 上下载一次后分发)
|
||||
ARTIFACTS_DIR="$HOME/k8s-artifacts"
|
||||
CNI_VERSION="v1.3.0"
|
||||
CNI_TGZ="cni-plugins-linux-amd64-${CNI_VERSION}.tgz"
|
||||
|
||||
# 函数:在所有节点执行命令
|
||||
execute_on_all_nodes() {
|
||||
local command="$1"
|
||||
local description="$2"
|
||||
|
||||
echo "==== $description ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "在 $hostname ($ip) 执行: $command"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
bash -lc "$command"
|
||||
else
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@$ip "$command"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函数:传输文件到所有节点
|
||||
copy_to_all_nodes() {
|
||||
local file="$1"
|
||||
echo "==== 传输文件 $file 到所有节点 ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "传输到 $hostname ($ip)"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
cp -f "$file" ~/
|
||||
else
|
||||
scp $SSH_OPTS $SSH_ID "$file" ubuntu@$ip:~/
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 创建容器运行时安装脚本
|
||||
cat > k8s-install-containerd.sh << 'EOF_OUTER'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 安装容器运行时 (containerd) ===="
|
||||
|
||||
# 1. 安装 containerd
|
||||
echo "安装 containerd..."
|
||||
sudo apt update
|
||||
sudo apt install -y containerd
|
||||
|
||||
# 2. 配置 containerd
|
||||
echo "配置 containerd..."
|
||||
# ① 停止 containerd
|
||||
sudo systemctl stop containerd
|
||||
|
||||
# ② 生成默认配置
|
||||
sudo mkdir -p /etc/containerd
|
||||
sudo containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
|
||||
|
||||
# ③ 注入镜像加速配置(docker.io/quay.io:腾讯云,其它:高校镜像优先)
|
||||
sudo sed -i '/\[plugins."io.containerd.grpc.v1.cri".registry.mirrors\]/a\
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]\n endpoint = ["https://mirror.ccs.tencentyun.com"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]\n endpoint = ["https://quay.tencentcloudcr.com"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]\n endpoint = ["https://ghcr.nju.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]\n endpoint = ["https://gcr.nju.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]\n endpoint = ["https://registry-k8s-io.mirrors.sjtug.sjtu.edu.cn"]\n [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]\n endpoint = ["https://gcr.nju.edu.cn"]' /etc/containerd/config.toml
|
||||
|
||||
# ④ 重新加载并启动 containerd
|
||||
sudo systemctl daemon-reexec
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart containerd
|
||||
|
||||
## 4. 在 master 预下载 CNI 压缩包并分发到各节点
|
||||
echo "准备 CNI 工件并分发..."
|
||||
if [ "$LOCAL_IP" = "172.17.0.15" ]; then
|
||||
mkdir -p "$ARTIFACTS_DIR"
|
||||
if [ ! -f "$ARTIFACTS_DIR/$CNI_TGZ" ]; then
|
||||
echo "在 master 下载 $CNI_TGZ ..."
|
||||
curl -L --fail --retry 3 --connect-timeout 10 \
|
||||
-o "$ARTIFACTS_DIR/$CNI_TGZ" \
|
||||
"https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/$CNI_TGZ"
|
||||
else
|
||||
echo "已存在 $ARTIFACTS_DIR/$CNI_TGZ,跳过下载"
|
||||
fi
|
||||
# 分发到所有节点 home 目录
|
||||
copy_to_all_nodes "$ARTIFACTS_DIR/$CNI_TGZ"
|
||||
fi
|
||||
|
||||
# 5. 安装 CNI 插件(优先使用已分发的本地文件)
|
||||
echo "安装 CNI 插件..."
|
||||
sudo mkdir -p /opt/cni/bin
|
||||
if [ -f "$CNI_TGZ" ]; then
|
||||
echo "使用已分发的 $CNI_TGZ 进行安装"
|
||||
sudo tar -xzf "$CNI_TGZ" -C /opt/cni/bin/
|
||||
rm -f "$CNI_TGZ"
|
||||
else
|
||||
echo "未找到本地 $CNI_TGZ,尝试在线下载(网络慢时可能用时较长)..."
|
||||
curl -L --fail --retry 3 --connect-timeout 10 \
|
||||
-o "$CNI_TGZ" \
|
||||
"https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/$CNI_TGZ"
|
||||
sudo tar -xzf "$CNI_TGZ" -C /opt/cni/bin/
|
||||
rm -f "$CNI_TGZ"
|
||||
fi
|
||||
|
||||
# 6. 验证安装
|
||||
echo "==== 验证 containerd 安装 ===="
|
||||
sudo systemctl status containerd --no-pager -l
|
||||
sudo ctr version
|
||||
|
||||
echo "==== containerd 安装完成 ===="
|
||||
EOF_OUTER
|
||||
|
||||
chmod +x k8s-install-containerd.sh
|
||||
copy_to_all_nodes k8s-install-containerd.sh
|
||||
execute_on_all_nodes "./k8s-install-containerd.sh" "安装容器运行时"
|
||||
|
||||
echo "==== 容器运行时安装完成 ===="
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 组件安装脚本
|
||||
# 功能: 在所有节点安装 kubelet, kubeadm, kubectl
|
||||
|
||||
echo "==== 安装 Kubernetes 组件 ===="
|
||||
|
||||
# 定义节点列表
|
||||
NODES=("172.17.0.15:master" "172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 本机 IP 与 SSH 选项
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
# SSH 私钥(可用环境变量 SSH_KEY 覆盖),存在则自动携带
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
|
||||
# 函数:在所有节点执行命令
|
||||
execute_on_all_nodes() {
|
||||
local command="$1"
|
||||
local description="$2"
|
||||
|
||||
echo "==== $description ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "在 $hostname ($ip) 执行: $command"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
bash -lc "$command"
|
||||
else
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@$ip "$command"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函数:传输文件到所有节点
|
||||
copy_to_all_nodes() {
|
||||
local file="$1"
|
||||
echo "==== 传输文件 $file 到所有节点 ===="
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "传输到 $hostname ($ip)"
|
||||
if [ "$ip" = "$LOCAL_IP" ] || [ "$hostname" = "master" ]; then
|
||||
cp -f "$file" ~/
|
||||
else
|
||||
scp $SSH_OPTS $SSH_ID "$file" ubuntu@$ip:~/
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 创建 Kubernetes 组件安装脚本
|
||||
cat > k8s-install-components.sh << 'EOF_OUTER'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 安装 Kubernetes 组件 ===="
|
||||
|
||||
# 1. 添加 Kubernetes 仓库
|
||||
echo "添加 Kubernetes 仓库 (pkgs.k8s.io v1.32)..."
|
||||
# 确保 keyrings 目录存在并可读
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list >/dev/null
|
||||
|
||||
# 2. 更新包列表
|
||||
echo "更新包列表..."
|
||||
sudo apt update
|
||||
|
||||
# 3. 安装 Kubernetes 组件(使用 v1.32 通道的最新补丁版本)
|
||||
echo "安装 Kubernetes 组件..."
|
||||
sudo apt install -y kubelet kubeadm kubectl
|
||||
|
||||
# 4. 锁定版本防止自动更新
|
||||
echo "锁定 Kubernetes 版本..."
|
||||
sudo apt-mark hold kubelet kubeadm kubectl
|
||||
|
||||
# 5. 配置 kubelet
|
||||
echo "配置 kubelet..."
|
||||
sudo mkdir -p /var/lib/kubelet
|
||||
cat <<EOF_KUBELET | sudo tee /var/lib/kubelet/config.yaml
|
||||
apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
authentication:
|
||||
anonymous:
|
||||
enabled: false
|
||||
webhook:
|
||||
enabled: true
|
||||
x509:
|
||||
clientCAFile: /etc/kubernetes/pki/ca.crt
|
||||
authorization:
|
||||
mode: Webhook
|
||||
clusterDomain: cluster.local
|
||||
clusterDNS:
|
||||
- 10.96.0.10
|
||||
containerRuntimeEndpoint: unix:///var/run/containerd/containerd.sock
|
||||
cgroupDriver: systemd
|
||||
failSwapOn: false
|
||||
hairpinMode: promiscuous-bridge
|
||||
healthzBindAddress: 127.0.0.1
|
||||
healthzPort: 10248
|
||||
httpCheckFrequency: 20s
|
||||
imageMinimumGCAge: 2m0s
|
||||
imageGCHighThresholdPercent: 85
|
||||
imageGCLowThresholdPercent: 80
|
||||
iptablesDropBit: 15
|
||||
iptablesMasqueradeBit: 15
|
||||
kubeAPIBurst: 10
|
||||
kubeAPIQPS: 5
|
||||
makeIPTablesUtilChains: true
|
||||
maxOpenFiles: 1000000
|
||||
maxPods: 110
|
||||
nodeStatusUpdateFrequency: 10s
|
||||
oomScoreAdj: -999
|
||||
podCIDR: 10.244.0.0/16
|
||||
registryBurst: 10
|
||||
registryPullQPS: 5
|
||||
resolvConf: /etc/resolv.conf
|
||||
rotateCertificates: true
|
||||
runtimeRequestTimeout: 2m0s
|
||||
serializeImagePulls: true
|
||||
serverTLSBootstrap: true
|
||||
streamingConnectionIdleTimeout: 4h0m0s
|
||||
syncFrequency: 1m0s
|
||||
volumeStatsAggPeriod: 1m0s
|
||||
EOF_KUBELET
|
||||
|
||||
# 6. 启动 kubelet
|
||||
echo "启动 kubelet..."
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable kubelet
|
||||
sudo systemctl start kubelet
|
||||
|
||||
# 7. 验证安装
|
||||
echo "==== 验证 Kubernetes 组件安装 ===="
|
||||
kubelet --version
|
||||
kubeadm version
|
||||
kubectl version --client
|
||||
|
||||
echo "==== Kubernetes 组件安装完成 ===="
|
||||
EOF_OUTER
|
||||
|
||||
chmod +x k8s-install-components.sh
|
||||
copy_to_all_nodes k8s-install-components.sh
|
||||
execute_on_all_nodes "./k8s-install-components.sh" "安装 Kubernetes 组件"
|
||||
|
||||
echo "==== Kubernetes 组件安装完成 ===="
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 集群初始化脚本
|
||||
# 功能: 在 Master 节点初始化 Kubernetes 集群
|
||||
|
||||
echo "==== 初始化 Kubernetes 集群 ===="
|
||||
|
||||
# 1. 初始化集群
|
||||
echo "初始化 Kubernetes 集群..."
|
||||
sudo kubeadm init \
|
||||
--apiserver-advertise-address=172.17.0.15 \
|
||||
--control-plane-endpoint=172.17.0.15:6443 \
|
||||
--kubernetes-version=v1.32.3 \
|
||||
--service-cidr=10.96.0.0/12 \
|
||||
--pod-network-cidr=10.244.0.0/16 \
|
||||
--image-repository=registry.aliyuncs.com/google_containers \
|
||||
--upload-certs \
|
||||
--ignore-preflight-errors=Swap
|
||||
|
||||
# 2. 配置 kubectl
|
||||
echo "配置 kubectl..."
|
||||
mkdir -p $HOME/.kube
|
||||
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
|
||||
sudo chown $(id -u):$(id -g) $HOME/.kube/config
|
||||
|
||||
# 3. 生成节点加入命令
|
||||
echo "生成节点加入命令..."
|
||||
JOIN_COMMAND=$(kubeadm token create --print-join-command)
|
||||
echo "节点加入命令:"
|
||||
echo "$JOIN_COMMAND"
|
||||
echo "$JOIN_COMMAND" > node-join-command.txt
|
||||
|
||||
# 4. 验证集群状态
|
||||
echo "==== 验证集群状态 ===="
|
||||
kubectl get nodes
|
||||
kubectl get pods -n kube-system
|
||||
|
||||
echo "==== 集群初始化完成 ===="
|
||||
echo "请保存节点加入命令,稍后用于将 node1 和 node2 加入集群"
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 网络插件安装脚本
|
||||
# 功能: 在 Master 节点安装 Flannel 网络插件
|
||||
|
||||
echo "==== 安装 Flannel 网络插件 ===="
|
||||
|
||||
# 1. 下载 Flannel 配置文件
|
||||
echo "下载 Flannel 配置文件..."
|
||||
FLANNEL_VER="v0.27.4"
|
||||
curl -fsSL https://raw.githubusercontent.com/flannel-io/flannel/${FLANNEL_VER}/Documentation/kube-flannel.yml -O
|
||||
|
||||
# 2. 修改 Flannel 配置
|
||||
echo "修改 Flannel 配置..."
|
||||
sed -i 's|"Network": "10.244.0.0/16"|"Network": "10.244.0.0/16"|g' kube-flannel.yml
|
||||
|
||||
echo "预拉取 Flannel 相关镜像(优先国内镜像域名,拉取后回标官方名)..."
|
||||
DOCKER_MIRROR="docker.m.daocloud.io"
|
||||
REGISTRY_K8S_MIRROR="registry-k8s-io.mirrors.sjtug.sjtu.edu.cn"
|
||||
GHCR_MIRROR="ghcr.tencentcloudcr.com"
|
||||
|
||||
IMAGES=(
|
||||
"registry.k8s.io/pause:3.8"
|
||||
"ghcr.io/flannel-io/flannel:${FLANNEL_VER}"
|
||||
)
|
||||
|
||||
pull_and_tag() {
|
||||
local origin_ref="$1" # e.g. registry.k8s.io/pause:3.8
|
||||
local mirror_ref="$2" # e.g. registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/pause:3.8
|
||||
echo "尝试从镜像 ${mirror_ref} 预拉取..."
|
||||
for i in $(seq 1 5); do
|
||||
if sudo ctr -n k8s.io images pull "${mirror_ref}"; then
|
||||
echo "打官方标签: ${origin_ref} <- ${mirror_ref}"
|
||||
sudo ctr -n k8s.io images tag "${mirror_ref}" "${origin_ref}" || true
|
||||
return 0
|
||||
fi
|
||||
echo "pull 失败,重试 ${i}/5..."; sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 预拉取 pause 镜像
|
||||
echo "预拉取: registry.k8s.io/pause:3.8"
|
||||
if pull_and_tag "registry.k8s.io/pause:3.8" "${REGISTRY_K8S_MIRROR}/pause:3.8"; then
|
||||
echo "pause 镜像拉取成功"
|
||||
else
|
||||
echo "WARN: pause 镜像拉取失败,将由 kubelet 重试"
|
||||
fi
|
||||
|
||||
# 预拉取 flannel 镜像
|
||||
echo "预拉取: ghcr.io/flannel-io/flannel:${FLANNEL_VER}"
|
||||
if pull_and_tag "ghcr.io/flannel-io/flannel:${FLANNEL_VER}" "${GHCR_MIRROR}/flannel-io/flannel:${FLANNEL_VER}"; then
|
||||
echo "flannel 镜像拉取成功"
|
||||
else
|
||||
echo "WARN: flannel 镜像拉取失败,将由 kubelet 重试"
|
||||
fi
|
||||
|
||||
# 3. 安装 Flannel
|
||||
echo "安装 Flannel..."
|
||||
kubectl apply -f kube-flannel.yml
|
||||
|
||||
# 4. 等待 Flannel 启动
|
||||
echo "等待 Flannel 组件就绪..."
|
||||
kubectl -n kube-flannel rollout status daemonset/kube-flannel-ds --timeout=600s || true
|
||||
kubectl wait --for=condition=ready pod -l app=flannel -n kube-flannel --timeout=600s || true
|
||||
|
||||
echo "等待 CoreDNS 由 Pending 变为 Ready..."
|
||||
kubectl -n kube-system rollout status deploy/coredns --timeout=600s || true
|
||||
|
||||
# 5. 验证网络插件
|
||||
echo "==== 验证 Flannel 安装 ===="
|
||||
kubectl get pods -n kube-flannel
|
||||
kubectl get nodes
|
||||
|
||||
echo "==== Flannel 网络插件安装完成 ===="
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Kubernetes 节点加入脚本
|
||||
# 功能: 将 Node1 和 Node2 加入 Kubernetes 集群
|
||||
|
||||
echo "==== 将节点加入 Kubernetes 集群 ===="
|
||||
|
||||
# 检查是否存在加入命令文件
|
||||
if [ ! -f "node-join-command.txt" ]; then
|
||||
echo "错误: 找不到 node-join-command.txt 文件"
|
||||
echo "请先运行 k8s-step4-init-cluster.sh 初始化集群"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 读取加入命令
|
||||
JOIN_COMMAND=$(cat node-join-command.txt)
|
||||
|
||||
# SSH 选项与密钥
|
||||
SSH_OPTS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
||||
SSH_KEY_PATH=${SSH_KEY:-$HOME/.ssh/id_rsa}
|
||||
[ -f "$SSH_KEY_PATH" ] && SSH_ID="-i $SSH_KEY_PATH" || SSH_ID=""
|
||||
echo "使用加入命令: $JOIN_COMMAND"
|
||||
|
||||
# 定义节点列表
|
||||
NODES=("172.17.0.43:node1" "172.17.0.34:node2")
|
||||
|
||||
# 将节点加入集群
|
||||
for node in "${NODES[@]}"; do
|
||||
IFS=':' read -r ip hostname <<< "$node"
|
||||
echo "==== 将 $hostname ($ip) 加入集群 ===="
|
||||
ssh $SSH_OPTS $SSH_ID ubuntu@$ip "sudo $JOIN_COMMAND"
|
||||
echo "$hostname 加入完成"
|
||||
done
|
||||
|
||||
# 等待节点加入
|
||||
echo "==== 等待节点加入集群 ===="
|
||||
sleep 30
|
||||
|
||||
# 验证集群状态
|
||||
echo "==== 验证集群状态 ===="
|
||||
kubectl get nodes
|
||||
kubectl get pods -n kube-system
|
||||
kubectl get pods -n kube-flannel
|
||||
|
||||
echo "==== 节点加入完成 ===="
|
||||
echo "集群信息:"
|
||||
echo "- Master: 172.17.0.15"
|
||||
echo "- Node1: 172.17.0.43"
|
||||
echo "- Node2: 172.17.0.34"
|
||||
echo "- Kubernetes 版本: v1.32.3"
|
||||
echo "- 网络插件: Flannel"
|
||||
echo "- 容器运行时: containerd"
|
||||
@@ -1,211 +0,0 @@
|
||||
---
|
||||
kind: Namespace
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: kube-flannel
|
||||
labels:
|
||||
k8s-app: flannel
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
---
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: flannel
|
||||
name: flannel
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes/status
|
||||
verbs:
|
||||
- patch
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: flannel
|
||||
name: flannel
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: flannel
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: flannel
|
||||
namespace: kube-flannel
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: flannel
|
||||
name: flannel
|
||||
namespace: kube-flannel
|
||||
---
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: kube-flannel-cfg
|
||||
namespace: kube-flannel
|
||||
labels:
|
||||
tier: node
|
||||
k8s-app: flannel
|
||||
app: flannel
|
||||
data:
|
||||
cni-conf.json: |
|
||||
{
|
||||
"name": "cbr0",
|
||||
"cniVersion": "0.3.1",
|
||||
"plugins": [
|
||||
{
|
||||
"type": "flannel",
|
||||
"delegate": {
|
||||
"hairpinMode": true,
|
||||
"isDefaultGateway": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "portmap",
|
||||
"capabilities": {
|
||||
"portMappings": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
net-conf.json: |
|
||||
{
|
||||
"Network": "10.244.0.0/16",
|
||||
"EnableNFTables": false,
|
||||
"Backend": {
|
||||
"Type": "vxlan"
|
||||
}
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: kube-flannel-ds
|
||||
namespace: kube-flannel
|
||||
labels:
|
||||
tier: node
|
||||
app: flannel
|
||||
k8s-app: flannel
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: flannel
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
tier: node
|
||||
app: flannel
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
hostNetwork: true
|
||||
priorityClassName: system-node-critical
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
effect: NoSchedule
|
||||
serviceAccountName: flannel
|
||||
initContainers:
|
||||
- name: install-cni-plugin
|
||||
image: ghcr.io/flannel-io/flannel-cni-plugin:v1.8.0-flannel1
|
||||
command:
|
||||
- cp
|
||||
args:
|
||||
- -f
|
||||
- /flannel
|
||||
- /opt/cni/bin/flannel
|
||||
volumeMounts:
|
||||
- name: cni-plugin
|
||||
mountPath: /opt/cni/bin
|
||||
- name: install-cni
|
||||
image: ghcr.io/flannel-io/flannel:v0.27.4
|
||||
command:
|
||||
- cp
|
||||
args:
|
||||
- -f
|
||||
- /etc/kube-flannel/cni-conf.json
|
||||
- /etc/cni/net.d/10-flannel.conflist
|
||||
volumeMounts:
|
||||
- name: cni
|
||||
mountPath: /etc/cni/net.d
|
||||
- name: flannel-cfg
|
||||
mountPath: /etc/kube-flannel/
|
||||
containers:
|
||||
- name: kube-flannel
|
||||
image: ghcr.io/flannel-io/flannel:v0.27.4
|
||||
command:
|
||||
- /opt/bin/flanneld
|
||||
args:
|
||||
- --ip-masq
|
||||
- --kube-subnet-mgr
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "50Mi"
|
||||
securityContext:
|
||||
privileged: false
|
||||
capabilities:
|
||||
add: ["NET_ADMIN", "NET_RAW"]
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: EVENT_QUEUE_DEPTH
|
||||
value: "5000"
|
||||
- name: CONT_WHEN_CACHE_NOT_READY
|
||||
value: "false"
|
||||
volumeMounts:
|
||||
- name: run
|
||||
mountPath: /run/flannel
|
||||
- name: flannel-cfg
|
||||
mountPath: /etc/kube-flannel/
|
||||
- name: xtables-lock
|
||||
mountPath: /run/xtables.lock
|
||||
volumes:
|
||||
- name: run
|
||||
hostPath:
|
||||
path: /run/flannel
|
||||
- name: cni-plugin
|
||||
hostPath:
|
||||
path: /opt/cni/bin
|
||||
- name: cni
|
||||
hostPath:
|
||||
path: /etc/cni/net.d
|
||||
- name: flannel-cfg
|
||||
configMap:
|
||||
name: kube-flannel-cfg
|
||||
- name: xtables-lock
|
||||
hostPath:
|
||||
path: /run/xtables.lock
|
||||
type: FileOrCreate
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 配置 Master 节点作为网关 ===="
|
||||
|
||||
# 1. 启用 IP 转发
|
||||
echo "启用 IP 转发..."
|
||||
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
|
||||
# 2. 配置 iptables NAT 规则
|
||||
echo "配置 iptables NAT 规则..."
|
||||
# 清空现有规则
|
||||
sudo iptables -F
|
||||
sudo iptables -t nat -F
|
||||
sudo iptables -t mangle -F
|
||||
sudo iptables -X
|
||||
sudo iptables -t nat -X
|
||||
sudo iptables -t mangle -X
|
||||
|
||||
# 设置默认策略
|
||||
sudo iptables -P INPUT ACCEPT
|
||||
sudo iptables -P FORWARD ACCEPT
|
||||
sudo iptables -P OUTPUT ACCEPT
|
||||
|
||||
# 配置 NAT 规则 - 允许内网节点通过 master 访问外网
|
||||
sudo iptables -t nat -A POSTROUTING -s 172.17.0.0/20 -o eth0 -j MASQUERADE
|
||||
|
||||
# 允许转发来自内网的流量
|
||||
sudo iptables -A FORWARD -s 172.17.0.0/20 -j ACCEPT
|
||||
sudo iptables -A FORWARD -d 172.17.0.0/20 -j ACCEPT
|
||||
|
||||
# 3. 保存 iptables 规则
|
||||
echo "保存 iptables 规则..."
|
||||
sudo apt update
|
||||
sudo apt install -y iptables-persistent
|
||||
sudo netfilter-persistent save
|
||||
|
||||
# 4. 验证配置
|
||||
echo "==== 验证配置 ===="
|
||||
echo "IP 转发状态:"
|
||||
cat /proc/sys/net/ipv4/ip_forward
|
||||
|
||||
echo "当前 iptables NAT 规则:"
|
||||
sudo iptables -t nat -L -n -v
|
||||
|
||||
echo "当前 iptables FORWARD 规则:"
|
||||
sudo iptables -L FORWARD -n -v
|
||||
|
||||
echo "==== Master 网关配置完成 ===="
|
||||
echo "Master 节点现在可以作为内网节点的网关使用"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 配置 Node1 (172.17.0.43) 网络路由 ===="
|
||||
|
||||
echo "==== 当前状态 ===="
|
||||
echo "当前主机名: $(hostname)"
|
||||
echo "当前 IP: $(ip route get 1 | awk '{print $7; exit}')"
|
||||
|
||||
# 配置网络路由 - 通过 master 访问外网
|
||||
echo "配置网络路由..."
|
||||
# 删除默认网关(如果存在)
|
||||
sudo ip route del default 2>/dev/null || true
|
||||
|
||||
# 添加默认网关指向 master
|
||||
sudo ip route add default via 172.17.0.15
|
||||
|
||||
echo "==== 验证网络配置 ===="
|
||||
echo "当前路由表:"
|
||||
ip route show
|
||||
|
||||
echo "测试网络连通性:"
|
||||
ping -c 2 172.17.0.15 && echo "✓ 可以访问 master" || echo "✗ 无法访问 master"
|
||||
ping -c 2 8.8.8.8 && echo "✓ 可以访问外网" || echo "✗ 无法访问外网"
|
||||
|
||||
echo "==== Node1 网络路由配置完成 ===="
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==== 配置 Node2 (172.17.0.34) 网络路由 ===="
|
||||
|
||||
echo "==== 当前状态 ===="
|
||||
echo "当前主机名: $(hostname)"
|
||||
echo "当前 IP: $(ip route get 1 | awk '{print $7; exit}')"
|
||||
|
||||
# 配置网络路由 - 通过 master 访问外网
|
||||
echo "配置网络路由..."
|
||||
# 删除默认网关(如果存在)
|
||||
sudo ip route del default 2>/dev/null || true
|
||||
|
||||
# 添加默认网关指向 master
|
||||
sudo ip route add default via 172.17.0.15
|
||||
|
||||
echo "==== 验证网络配置 ===="
|
||||
echo "当前路由表:"
|
||||
ip route show
|
||||
|
||||
echo "测试网络连通性:"
|
||||
ping -c 2 172.17.0.15 && echo "✓ 可以访问 master" || echo "✗ 无法访问 master"
|
||||
ping -c 2 8.8.8.8 && echo "✓ 可以访问外网" || echo "✗ 无法访问外网"
|
||||
|
||||
echo "==== Node2 网络路由配置完成 ===="
|
||||
136
models/actions/debug_session.go
Normal file
136
models/actions/debug_session.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// DebugSession represents a workflow debug session
|
||||
type DebugSession struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"index"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
CreatorID int64 `xorm:"index"`
|
||||
Creator *user_model.User `xorm:"-"`
|
||||
WorkflowID string // workflow file name
|
||||
WorkflowContent string `xorm:"LONGTEXT"` // edited workflow YAML content
|
||||
Status string `xorm:"index"` // draft/running/success/failed/cancelled
|
||||
RunID int64 `xorm:"-"` // the actual run ID when executed (0 if not run yet)
|
||||
DebugParams string `xorm:"TEXT"` // JSON: {ref, event, inputs, env}
|
||||
ErrorMsg string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created index"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
ExpiresUnix timeutil.TimeStamp // auto cleanup after expiration
|
||||
}
|
||||
|
||||
// TableName sets the table name
|
||||
func (DebugSession) TableName() string {
|
||||
return "action_debug_session"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(DebugSession))
|
||||
}
|
||||
|
||||
// DebugSessionStatus represents the possible statuses of a debug session
|
||||
const (
|
||||
DebugSessionStatusDraft = "draft"
|
||||
DebugSessionStatusRunning = "running"
|
||||
DebugSessionStatusSuccess = "success"
|
||||
DebugSessionStatusFailed = "failed"
|
||||
DebugSessionStatusCancelled = "cancelled"
|
||||
)
|
||||
|
||||
// CreateDebugSession creates a new debug session
|
||||
func CreateDebugSession(ctx context.Context, session *DebugSession) error {
|
||||
session.Status = DebugSessionStatusDraft
|
||||
session.CreatedUnix = timeutil.TimeStampNow()
|
||||
session.UpdatedUnix = timeutil.TimeStampNow()
|
||||
session.ExpiresUnix = timeutil.TimeStampNow().AddDuration(time.Hour * 24) // expire in 24 hours
|
||||
_, err := db.GetEngine(ctx).Insert(session)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDebugSession gets a debug session by ID
|
||||
func GetDebugSession(ctx context.Context, id int64) (*DebugSession, error) {
|
||||
session := &DebugSession{}
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, fmt.Errorf("debug session with id %d: %w", id, util.ErrNotExist)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// UpdateDebugSession updates a debug session
|
||||
func UpdateDebugSession(ctx context.Context, session *DebugSession) error {
|
||||
session.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).ID(session.ID).Update(session)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListDebugSessions lists debug sessions for a repository
|
||||
func ListDebugSessions(ctx context.Context, repoID int64) ([]*DebugSession, error) {
|
||||
var sessions []*DebugSession
|
||||
err := db.GetEngine(ctx).
|
||||
Where("repo_id = ?", repoID).
|
||||
Where("status != ?", DebugSessionStatusCancelled).
|
||||
OrderBy("created_unix DESC").
|
||||
Find(&sessions)
|
||||
return sessions, err
|
||||
}
|
||||
|
||||
// DeleteDebugSession deletes a debug session
|
||||
func DeleteDebugSession(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(&DebugSession{})
|
||||
return err
|
||||
}
|
||||
|
||||
// CleanupExpiredDebugSessions cleans up expired debug sessions
|
||||
func CleanupExpiredDebugSessions(ctx context.Context) error {
|
||||
now := timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Where(builder.Lt{"expires_unix": now}).Delete(&DebugSession{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DebugParams represents the parameters for a debug session
|
||||
type DebugParams struct {
|
||||
Ref string `json:"ref"` // branch/tag
|
||||
Event string `json:"event"` // push/pull_request/etc
|
||||
Inputs map[string]string `json:"inputs"` // workflow inputs
|
||||
Env map[string]string `json:"env"` // environment variables
|
||||
}
|
||||
|
||||
// ParseDebugParams parses the JSON debug params
|
||||
func (s *DebugSession) ParseDebugParams() (*DebugParams, error) {
|
||||
params := &DebugParams{}
|
||||
if s.DebugParams == "" {
|
||||
return params, nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(s.DebugParams), params)
|
||||
return params, err
|
||||
}
|
||||
|
||||
// SetDebugParams sets the debug params from a struct
|
||||
func (s *DebugSession) SetDebugParams(params *DebugParams) error {
|
||||
data, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.DebugParams = string(data)
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +125,7 @@ type User struct {
|
||||
AllowImportLocal bool // Allow migrate repository by local path
|
||||
AllowCreateOrganization bool `xorm:"DEFAULT true"`
|
||||
AllowCreateDevcontainer bool `xorm:"DEFAULT false"`
|
||||
AllowCreateActRunner bool `xorm:"DEFAULT false"`
|
||||
|
||||
// true: the user is not allowed to log in Web UI. Git/SSH access could still be allowed (please refer to Git/SSH access related code/documents)
|
||||
ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"`
|
||||
@@ -274,6 +275,11 @@ func (u *User) CanCreateDevcontainer() bool {
|
||||
return u.AllowCreateDevcontainer
|
||||
}
|
||||
|
||||
// CanCreateActrunner returns true if user can create organisation.
|
||||
func (u *User) CanCreateActrunner() bool {
|
||||
return u.AllowCreateActRunner
|
||||
}
|
||||
|
||||
// CanEditGitHook returns true if user can edit Git hooks.
|
||||
func (u *User) CanEditGitHook() bool {
|
||||
return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)
|
||||
@@ -640,6 +646,7 @@ type CreateUserOverwriteOptions struct {
|
||||
Visibility *structs.VisibleType
|
||||
AllowCreateOrganization optional.Option[bool]
|
||||
AllowCreateDevcontainer optional.Option[bool]
|
||||
AllowCreateActRunner optional.Option[bool]
|
||||
EmailNotificationsPreference *string
|
||||
MaxRepoCreation *int
|
||||
Theme *string
|
||||
@@ -667,6 +674,8 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
|
||||
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
||||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
||||
u.AllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
||||
u.AllowCreateActRunner = setting.Service.DefaultAllowCreateActRunner
|
||||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
||||
u.MaxRepoCreation = -1
|
||||
u.Theme = setting.UI.DefaultTheme
|
||||
|
||||
@@ -59,6 +59,7 @@ func NewActionsUser() *User {
|
||||
Type: UserTypeBot,
|
||||
AllowCreateOrganization: true,
|
||||
AllowCreateDevcontainer: false,
|
||||
AllowCreateActRunner: false,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,24 +89,22 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
state := containerInfo.State
|
||||
return state.Status, nil
|
||||
}
|
||||
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
||||
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
||||
cmd := exec.Command("sh", "-c", script)
|
||||
_, err := cmd.CombinedOutput()
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
|
||||
}
|
||||
// 推送到仓库
|
||||
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
||||
cmd = exec.Command("sh", "-c", script)
|
||||
_, err = cmd.CombinedOutput()
|
||||
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ var Service = struct {
|
||||
McaptchaURL string
|
||||
DefaultKeepEmailPrivate bool
|
||||
DefaultAllowCreateOrganization bool
|
||||
DefaultAllowCreateDevcontainer bool
|
||||
DefaultAllowCreateActRunner bool
|
||||
DefaultUserIsRestricted bool
|
||||
EnableTimetracking bool
|
||||
DefaultEnableTimetracking bool
|
||||
@@ -205,6 +207,8 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||
Service.DefaultAllowCreateDevcontainer = sec.Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").MustBool(true)
|
||||
Service.DefaultAllowCreateActRunner = sec.Key("DEFAULT_ALLOW_CREATE_ACTRUNNER").MustBool(false)
|
||||
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||
if Service.EnableTimetracking {
|
||||
|
||||
@@ -55,6 +55,7 @@ type EditUserOption struct {
|
||||
ProhibitLogin *bool `json:"prohibit_login"`
|
||||
AllowCreateOrganization *bool `json:"allow_create_organization"`
|
||||
AllowCreateDevcontainer *bool `json:"allow_create_devcontainer"`
|
||||
AllowCreateActRunner *bool `json:"allow_create_actrunner"`
|
||||
Restricted *bool `json:"restricted"`
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
}
|
||||
|
||||
@@ -245,8 +245,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Instalace
|
||||
installing_desc=Probíhá instalace, čekejte prosím...
|
||||
title=Výchozí konfigurace
|
||||
docker_helper=Pokud spouštíte Gitea v Dockeru, přečtěte si <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>, než budete měnit jakákoliv nastavení.
|
||||
require_db_desc=Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
k8s_helper=Pokud spouštíte DevStar v Kubernetesu, přečtěte si <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>, než budete měnit jakákoliv nastavení.
|
||||
require_db_desc=DevStar requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
db_title=Nastavení databáze
|
||||
db_type=Typ databáze
|
||||
host=Hostitel
|
||||
@@ -351,7 +351,7 @@ password_algorithm=Hash algoritmus hesla
|
||||
invalid_password_algorithm=Neplatný algoritmus hash hesla
|
||||
password_algorithm_helper=Nastavte algoritmus hashování hesla. Algoritmy mají odlišné požadavky a sílu. Algoritmus argon2 je poměrně bezpečný, ale používá spoustu paměti a může být nevhodný pro malé systémy.
|
||||
enable_update_checker=Povolit kontrolu aktualizací
|
||||
enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io.
|
||||
enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke devstar.cn.
|
||||
env_config_keys=Konfigurace prostředí
|
||||
env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor:
|
||||
config_write_file_prompt=Tyto možnosti konfigurace budou zapsány do: %s
|
||||
|
||||
@@ -251,8 +251,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Installation
|
||||
installing_desc=Wird jetzt installiert, bitte warten...
|
||||
title=Erstkonfiguration
|
||||
docker_helper=Wenn du Gitea in einem Docker-Container nutzt, lies bitte die <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>, bevor du irgendwelche Einstellungen veränderst.
|
||||
require_db_desc=Gitea benötigt MySQL, PostgreSQL, MSSQL, SQLite3 oder TiDB (MySQL-Protokoll).
|
||||
k8s_helper=Wenn du DevStar in einem Kubernetes-Container nutzt, lies bitte die <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>, bevor du irgendwelche Einstellungen veränderst.
|
||||
require_db_desc=DevStar benötigt MySQL, PostgreSQL, MSSQL, SQLite3 oder TiDB (MySQL-Protokoll).
|
||||
db_title=Datenbankeinstellungen
|
||||
db_type=Datenbanktyp
|
||||
host=Host
|
||||
@@ -357,7 +357,7 @@ password_algorithm=Passwort Hashing Algorithmus
|
||||
invalid_password_algorithm=Ungültiger Passwort-Hash-Algorithmus
|
||||
password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein.
|
||||
enable_update_checker=Aktualisierungsprüfung aktivieren
|
||||
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.
|
||||
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu devstar.cn her, um nach neuen Versionen zu prüfen.
|
||||
env_config_keys=Umgebungskonfiguration
|
||||
env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
|
||||
config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben
|
||||
|
||||
@@ -203,8 +203,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Εγκατάσταση
|
||||
title=Αρχικές Ρυθμίσεις
|
||||
docker_helper=Αν εκτελέσετε το Gitea μέσα στο Docker, παρακαλώ διαβάστε την <a target="_blank" rel="noopener noreferrer" href="%s">τεκμηρίωση</a> πριν αλλάξετε τις ρυθμίσεις.
|
||||
require_db_desc=Το Gitea απαιτεί MySQL, PostgreSQL, MSSQL, SQLite3 ή TiDB (με πρωτόκολλο MySQL).
|
||||
k8s_helper=Αν εκτελέσετε το DevStar μέσα στο Kubernetes, παρακαλώ διαβάστε την <a target="_blank" rel="noopener noreferrer" href="%s">τεκμηρίωση</a> πριν αλλάξετε τις ρυθμίσεις.
|
||||
require_db_desc=Το DevStar απαιτεί MySQL, PostgreSQL, MSSQL, SQLite3 ή TiDB (με πρωτόκολλο MySQL).
|
||||
db_title=Ρυθμίσεις Βάσης Δεδομένων
|
||||
db_type=Τύπος της Βάσης Δεδομένων
|
||||
host=Διακομιστής
|
||||
@@ -308,7 +308,7 @@ password_algorithm=Αλγόριθμος Hash Κωδικού Πρόσβασης
|
||||
invalid_password_algorithm=Μη έγκυρος αλγόριθμος κωδικού πρόσβασης
|
||||
password_algorithm_helper=Ορίστε τον αλγόριθμο κατακερματισμού για το κωδικό πρόσβασης. Οι αλγόριθμοι διαφέρουν σε απαιτήσεις και αντοχή. Ο αλγόριθμος argon2 είναι αρκετά ασφαλής, αλλά χρησιμοποιεί πολλή μνήμη και μπορεί να είναι ακατάλληλος για μικρά συστήματα.
|
||||
enable_update_checker=Ενεργοποίηση Ελεγκτή Ενημερώσεων
|
||||
enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο gitea.io.
|
||||
enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο devstar.cn.
|
||||
env_config_keys=Ρυθμίσεις Περιβάλλοντος
|
||||
env_config_keys_prompt=Οι ακόλουθες μεταβλητές περιβάλλοντος θα εφαρμοστούν επίσης στο αρχείο ρυθμίσεων σας:
|
||||
|
||||
|
||||
@@ -256,8 +256,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install = Installation
|
||||
installing_desc = Installing now, please wait...
|
||||
title = Initial Configuration
|
||||
docker_helper = If you run Gitea inside Docker, please read the <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> before changing any settings.
|
||||
require_db_desc = Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
k8s_helper = If you run DevStar inside Kubernetes, please read the <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> before changing any settings.
|
||||
require_db_desc = DevStar requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
db_title = Database Settings
|
||||
db_type = Database Type
|
||||
host = Host
|
||||
@@ -362,7 +362,11 @@ invalid_log_root_path = The log path is invalid: %v
|
||||
default_keep_email_private = Hide Email Addresses by Default
|
||||
default_keep_email_private_popup = Hide email addresses of new user accounts by default.
|
||||
default_allow_create_organization = Allow Creation of Organizations by Default
|
||||
default_allow_create_devcontainer = Allow Creation of DevContainers by Default
|
||||
default_allow_create_actrunner = Allow Creation of ActionRunners by Default
|
||||
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
||||
default_allow_create_devcontainer_popup = Allow new user accounts to create devcontainers by default.
|
||||
default_allow_create_actrunner_popup = Allow new user accounts to create ActionRunner by default.
|
||||
default_enable_timetracking = Enable Time Tracking by Default
|
||||
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
||||
no_reply_address = Hidden Email Domain
|
||||
@@ -371,7 +375,7 @@ password_algorithm = Password Hash Algorithm
|
||||
invalid_password_algorithm = Invalid password hash algorithm
|
||||
password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems.
|
||||
enable_update_checker = Enable Update Checker
|
||||
enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io.
|
||||
enable_update_checker_helper = Checks for new version releases periodically by connecting to devstar.cn
|
||||
env_config_keys = Environment Configuration
|
||||
env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
|
||||
config_write_file_prompt = These configuration options will be written into: %s
|
||||
@@ -3160,6 +3164,7 @@ users.allow_git_hook_tooltip = Git Hooks are executed as the OS user running Git
|
||||
users.allow_import_local = May Import Local Repositories
|
||||
users.allow_create_organization = May Create Organizations
|
||||
users.allow_create_devcontainer= May Create Devcontainers
|
||||
users.allow_create_actrunner= May Create ActRunners
|
||||
users.update_profile = Update User Account
|
||||
users.delete_account = Delete User Account
|
||||
users.cannot_delete_self = "You cannot delete yourself"
|
||||
@@ -3420,6 +3425,7 @@ config.active_code_lives = Active Code Lives
|
||||
config.reset_password_code_lives = Recover Account Code Expiry Time
|
||||
config.default_keep_email_private = Hide Email Addresses by Default
|
||||
config.default_allow_create_organization = Allow Creation of Organizations by Default
|
||||
config.default_allow_create_devcontainer = Allow Creation of Dev Containers by Default
|
||||
config.enable_timetracking = Enable Time Tracking
|
||||
config.default_enable_timetracking = Enable Time Tracking by Default
|
||||
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
||||
@@ -3877,7 +3883,7 @@ status.blocked = "Blocked"
|
||||
runners = Runners
|
||||
runners.runner_manage_panel = Runners Management
|
||||
runners.new = Create new Runner
|
||||
runners.new_notice = How to start a runner
|
||||
runners.new_notice = How to start a runner manually
|
||||
runners.status = Status
|
||||
runners.id = ID
|
||||
runners.name = Name
|
||||
@@ -3952,6 +3958,20 @@ workflow.run_success = Workflow '%s' run successfully.
|
||||
workflow.from_ref = Use workflow from
|
||||
workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger.
|
||||
workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger.
|
||||
workflow.debug = Debug Workflow
|
||||
workflow.content = Workflow Content
|
||||
workflow.ref = Reference (Branch/Tag)
|
||||
workflow.event = Event Type
|
||||
workflow.inputs = Workflow Inputs
|
||||
workflow.env = Environment Variables
|
||||
workflow.logs = Execution Logs
|
||||
workflow.content_empty = Workflow content cannot be empty
|
||||
workflow.logs_placeholder = Logs will appear here when workflow runs...
|
||||
workflow.running = Workflow is running
|
||||
workflow.input_name = Input Name
|
||||
workflow.input_value = Input Value
|
||||
workflow.env_name = Environment Name
|
||||
workflow.env_value = Environment Value
|
||||
|
||||
need_approval_desc = Need approval to run workflows for fork pull request.
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Instalación
|
||||
title=Configuración inicial
|
||||
docker_helper=Si está ejecutando Gitea dentro de un contenedor Docker, por favor lea la <a target="_blank" rel="noopener noreferrer" href="%s">documentación</a> antes de realizar cambios en la configuración.
|
||||
require_db_desc=Gitea requiere una base de datos MySQL, PostgreSQL, MSSQL, SQLite3 o TiDB (usar el protocolo MySQL).
|
||||
k8s_helper=Si está ejecutando DevStar dentro de un contenedor Kubernetes, por favor lea la <a target="_blank" rel="noopener noreferrer" href="%s">documentación</a> antes de realizar cambios en la configuración.
|
||||
require_db_desc=DevStar requiere una base de datos MySQL, PostgreSQL, MSSQL, SQLite3 o TiDB (usar el protocolo MySQL).
|
||||
db_title=Configuración de base de datos
|
||||
db_type=Tipo de base de datos
|
||||
host=Servidor
|
||||
@@ -306,7 +306,7 @@ password_algorithm=Algoritmo Hash de Contraseña
|
||||
invalid_password_algorithm=Algoritmo hash de contraseña no válido
|
||||
password_algorithm_helper=Establece el algoritmo de hashing de contraseña. Los algoritmos tienen diferentes requisitos y fuerza. El algoritmo argon2 es bastante seguro, pero usa mucha memoria y puede ser inapropiado para sistemas pequeños.
|
||||
enable_update_checker=Activar comprobador de actualizaciones
|
||||
enable_update_checker_helper=Comprueba el lanzamiento de nuevas versiones periódicamente en gitea.io.
|
||||
enable_update_checker_helper=Comprueba el lanzamiento de nuevas versiones periódicamente en devstar.cn.
|
||||
env_config_keys=Configuración del entorno
|
||||
env_config_keys_prompt=Las siguientes variables de entorno también se aplicarán a su archivo de configuración:
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=نصب و راه اندازی
|
||||
title=تنظیمات اولیه
|
||||
docker_helper=اگر گیتی را با داکر اجرا کردهاید، لطفا قبل از هر تغییری <a target="_blank" rel="noopener noreferrer" href="%s">مستندات</a> را مطالعه نمایید.
|
||||
k8s_helper=اگر گیتی را با داکر اجرا کردهاید، لطفا قبل از هر تغییری <a target="_blank" rel="noopener noreferrer" href="%s">مستندات</a> را مطالعه نمایید.
|
||||
db_title=تنظیمات پایگاه داده
|
||||
db_type=نوع پایگاه داده
|
||||
host=میزبان
|
||||
|
||||
@@ -152,8 +152,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Asennus
|
||||
title=Alkuperäiset asetukset
|
||||
docker_helper=Jos ajat Giteaa Dockerin sisällä, lue <a target="_blank" rel="noopener noreferrer" href="%s">ohjeet</a> ennen minkään asetuksen muuttamista.
|
||||
require_db_desc=Gitea tarvitsee toimiakseen MySQL, PostgreSQL, MSSQL, SQLite3 tai TiDB (MySQL protokolla) tietokannan.
|
||||
k8s_helper=Jos ajat DevStara Kubernetesin sisällä, lue <a target="_blank" rel="noopener noreferrer" href="%s">ohjeet</a> ennen minkään asetuksen muuttamista.
|
||||
require_db_desc=DevStar tarvitsee toimiakseen MySQL, PostgreSQL, MSSQL, SQLite3 tai TiDB (MySQL protokolla) tietokannan.
|
||||
db_title=Tietokanta asetukset
|
||||
db_type=Tietokanta tyyppi
|
||||
host=Isäntä
|
||||
|
||||
@@ -254,8 +254,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Installation
|
||||
installing_desc=Installation en cours, veuillez patienter…
|
||||
title=Configuration initiale
|
||||
docker_helper=Si vous exécutez Gitea dans Docker, veuillez lire la <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> avant de modifier les paramètres.
|
||||
require_db_desc=Gitea nécessite MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (avec le protocole MySQL).
|
||||
k8s_helper=Si vous exécutez DevStar dans Kubernetes, veuillez lire la <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> avant de modifier les paramètres.
|
||||
require_db_desc=DevStar nécessite MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (avec le protocole MySQL).
|
||||
db_title=Paramètres de la base de données
|
||||
db_type=Type de base de données
|
||||
host=Hôte
|
||||
@@ -360,7 +360,7 @@ password_algorithm=Algorithme de hachage du mot de passe
|
||||
invalid_password_algorithm=Algorithme de hachage du mot de passe invalide
|
||||
password_algorithm_helper=Définissez l’algorithme de hachage du mot de passe. Les algorithmes ont des exigences matérielles et une résistance différentes. L’algorithme argon2 est bien sécurisé mais utilise beaucoup de mémoire et peut être inapproprié pour les systèmes limités en ressources.
|
||||
enable_update_checker=Activer la vérification des mises-à-jour
|
||||
enable_update_checker_helper=Vérifie les mises à jour régulièrement en se connectant à gitea.io.
|
||||
enable_update_checker_helper=Vérifie les mises à jour régulièrement en se connectant à devstar.cn.
|
||||
env_config_keys=Configuration de l'environnement
|
||||
env_config_keys_prompt=Les variables d'environnement suivantes seront également ajoutées à votre fichier de configuration :
|
||||
config_write_file_prompt=Ces options de configuration seront écrites dans : %s
|
||||
|
||||
@@ -254,7 +254,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Suiteáil
|
||||
installing_desc=Suiteáil anois, fan go fóill...
|
||||
title=Cumraíocht Tosaigh
|
||||
docker_helper=Má ritheann tú Gitea taobh istigh de Docker, léigh an <a target="_blank" rel="noopener noreferrer" href="%s">doiciméadúchán</a> roimh aon socruithe a athrú.
|
||||
k8s_helper=Má ritheann tú DevStar taobh istigh de Kubernetes, léigh an <a target="_blank" rel="noopener noreferrer" href="%s">doiciméadúchán</a> roimh aon socruithe a athrú.
|
||||
require_db_desc=Éilíonn Gitea MySQL, PostgreSQL, MSSQL, SQLite3 nó TiDB (prótacal MySQL).
|
||||
db_title=Socruithe Bunachar Sonraí
|
||||
db_type=Cineál Bunachar Sonraí
|
||||
@@ -360,7 +360,7 @@ password_algorithm=Algartam Hais Pasfhocal
|
||||
invalid_password_algorithm=Algartam hais pasfhocail neamhbhailí
|
||||
password_algorithm_helper=Socraigh an algartam hashing pasfhocal. Tá riachtanais agus neart éagsúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith míchuí do chórais bheaga.
|
||||
enable_update_checker=Cumasaigh Seiceoir Nuashonraithe
|
||||
enable_update_checker_helper=Seiceálacha ar eisiúintí leagan nua go tréimhsiúil trí nascadh le gitea.io.
|
||||
enable_update_checker_helper=Seiceálacha ar eisiúintí leagan nua go tréimhsiúil trí nascadh le devstar.cn.
|
||||
env_config_keys=Cumraíocht Comhshaoil
|
||||
env_config_keys_prompt=Cuirfear na hathróga comhshaoil seo a leanas i bhfeidhm ar do chomhad cumraíochta freisin:
|
||||
config_write_file_prompt=Scríobhfar na roghanna cumraíochta seo isteach: %s
|
||||
|
||||
@@ -123,7 +123,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Telepítés
|
||||
title=Kezdeti konfiguráció
|
||||
docker_helper=Ha ön a Gitea-t Docker-ből futtatja, kérem olvassa el a <a target="_blank" rel="noopener noreferrer" href="%s">dokumentációt</a> a beállítások megváltoztatása előtt.
|
||||
k8s_helper=Ha ön a DevStar-t Kubernetes-ből futtatja, kérem olvassa el a <a target="_blank" rel="noopener noreferrer" href="%s">dokumentációt</a> a beállítások megváltoztatása előtt.
|
||||
db_title=Adatbázis beállítások
|
||||
db_type=Adatbázis típusa
|
||||
host=Kiszolgáló
|
||||
|
||||
@@ -148,8 +148,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Uppsetning
|
||||
title=Upphafleg Uppsetning
|
||||
docker_helper=Ef þú keyrir Gitea inni í Docker þá viltu vinsamlegast lesa <a target="_blank" rel="noopener noreferrer" href="%s">leiðbeiningaritið</a> áður en þú breytir stillingum.
|
||||
require_db_desc=Gitea krefst MySQL, PostgreSQL, MSSQL, SQLite3 eða TiDB (MySQL samskiptareglur).
|
||||
k8s_helper=Ef þú keyrir DevStar inni í Kubernetes þá viltu vinsamlegast lesa <a target="_blank" rel="noopener noreferrer" href="%s">leiðbeiningaritið</a> áður en þú breytir stillingum.
|
||||
require_db_desc=DevStar krefst MySQL, PostgreSQL, MSSQL, SQLite3 eða TiDB (MySQL samskiptareglur).
|
||||
db_title=Gagnagrunnsstillingar
|
||||
db_type=Tegund Gagnagrunns
|
||||
host=Hýsill
|
||||
|
||||
@@ -154,8 +154,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Installazione
|
||||
title=Configurazione Iniziale
|
||||
docker_helper=Se stai usando Gitea con Docker, leggi <a target="_blank" rel="noopener noreferrer" href="%s">la documentazione</a> prima di cambiare qualsiasi impostazione.
|
||||
require_db_desc=Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
k8s_helper=Se stai usando DevStar con Kubernetes, leggi <a target="_blank" rel="noopener noreferrer" href="%s">la documentazione</a> prima di cambiare qualsiasi impostazione.
|
||||
require_db_desc=DevStar requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
db_title=Impostazioni Database
|
||||
db_type=Tipo di database
|
||||
host=Host
|
||||
|
||||
@@ -251,8 +251,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=インストール
|
||||
installing_desc=インストール中です、お待ちください...
|
||||
title=初期設定
|
||||
docker_helper=GiteaをDocker内で実行する場合は、設定を変更する前に<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を読んでください。
|
||||
require_db_desc=Giteaには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。
|
||||
k8s_helper=DevStarをKubernetes内で実行する場合は、設定を変更する前に<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を読んでください。
|
||||
require_db_desc=DevStarには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。
|
||||
db_title=データベース設定
|
||||
db_type=データベースのタイプ
|
||||
host=ホスト
|
||||
@@ -357,7 +357,7 @@ password_algorithm=パスワードハッシュアルゴリズム
|
||||
invalid_password_algorithm=無効なパスワードハッシュアルゴリズム
|
||||
password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。
|
||||
enable_update_checker=アップデートチェッカーを有効にする
|
||||
enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。
|
||||
enable_update_checker_helper=devstar.cnに接続して定期的に新しいバージョンのリリースを確認します。
|
||||
env_config_keys=環境設定
|
||||
env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
|
||||
config_write_file_prompt=これらの設定オプションは %s に書き込まれます
|
||||
|
||||
@@ -115,7 +115,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=설치
|
||||
title=초기 설정
|
||||
docker_helper=Gitea를 Docker에서 실행하려면 설정 전에 이 <a target="_blank" rel="noopener noreferrer" href="%s">문서</a>를 읽어보세요.
|
||||
k8s_helper=DevStar를 Kubernetes에서 실행하려면 설정 전에 이 <a target="_blank" rel="noopener noreferrer" href="%s">문서</a>를 읽어보세요.
|
||||
db_title=데이터베이스 설정
|
||||
db_type=데이터베이스 유형
|
||||
host=호스트
|
||||
|
||||
@@ -206,8 +206,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Instalācija
|
||||
title=Sākotnējā konfigurācija
|
||||
docker_helper=Ja Gitea ir uzstādīts Docker konteinerī, izlasiet <a target="_blank" rel="noopener noreferrer" href="%s">vadlīninas</a> pirms maināt iestatījumus.
|
||||
require_db_desc=Gitea nepieciešams MySQL, PostgreSQL, MSSQL, SQLite3 vai TiDB (izmantojot MySQL protokolu).
|
||||
k8s_helper=Ja DevStar ir uzstādīts Kubernetes konteinerī, izlasiet <a target="_blank" rel="noopener noreferrer" href="%s">vadlīninas</a> pirms maināt iestatījumus.
|
||||
require_db_desc=DevStar nepieciešams MySQL, PostgreSQL, MSSQL, SQLite3 vai TiDB (izmantojot MySQL protokolu).
|
||||
db_title=Datu bāzes iestatījumi
|
||||
db_type=Datu bāzes veids
|
||||
host=Resursdators
|
||||
@@ -311,7 +311,7 @@ password_algorithm=Paroles jaucējsummas algoritms
|
||||
invalid_password_algorithm=Kļūdaina paroles jaucējfunkcija
|
||||
password_algorithm_helper=Norādiet paroles jaucējalgoritmu. Algoritmi atšķirās pēc prasībām pret resursiem un stipruma. Argon2 algoritms ir drošs, bet tam nepieciešams daudz operatīvās atmiņas, līdz ar ko tas var nebūt piemērots sistēmām ar maz pieejamajiem resursiem.
|
||||
enable_update_checker=Iespējot jaunu versiju paziņojumus
|
||||
enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no gitea.io.
|
||||
enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no devstar.cn.
|
||||
env_config_keys=Vides konfigurācija
|
||||
env_config_keys_prompt=Šie vides mainīgie tiks pielietoti arī konfigurācijas failā:
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Installatie
|
||||
title=Initiële configuratie
|
||||
docker_helper=Als je gitea draait in Docker, Lees eerst de <a target="_blank" rel="noopener noreferrer" href="%s">documentatie</a> voordat je een instelling aanpast.
|
||||
require_db_desc=Gitea vereist MySQL, PostgreSQL, MSSQL, SQLite3 of TiDB (MySQL protocol).
|
||||
k8s_helper=Als je gitea draait in Kubernetes, Lees eerst de <a target="_blank" rel="noopener noreferrer" href="%s">documentatie</a> voordat je een instelling aanpast.
|
||||
require_db_desc=DevStar vereist MySQL, PostgreSQL, MSSQL, SQLite3 of TiDB (MySQL protocol).
|
||||
db_title=Database-instellingen
|
||||
db_type=Database-type
|
||||
host=Server
|
||||
|
||||
@@ -149,8 +149,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Instalacja
|
||||
title=Wstępna konfiguracja
|
||||
docker_helper=Jeśli używasz Gitea za pomocą Docker'a, przeczytaj <a target="_blank" rel="noopener noreferrer" href="%s">dokumentację</a> przed wprowadzeniem jakichkolwiek zmian.
|
||||
require_db_desc=Gitea wymaga MySQL, PostgreSQL, MSSQL, SQLite3 lub TiDB (protokół MySQL).
|
||||
k8s_helper=Jeśli używasz DevStar za pomocą Kubernetes'a, przeczytaj <a target="_blank" rel="noopener noreferrer" href="%s">dokumentację</a> przed wprowadzeniem jakichkolwiek zmian.
|
||||
require_db_desc=DevStar wymaga MySQL, PostgreSQL, MSSQL, SQLite3 lub TiDB (protokół MySQL).
|
||||
db_title=Ustawienia bazy danych
|
||||
db_type=Typ bazy danych
|
||||
host=Serwer
|
||||
|
||||
@@ -202,8 +202,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Instalação
|
||||
title=Configuração inicial
|
||||
docker_helper=Se você está rodando o Gitea dentro do Docker, por favor leia a <a target="_blank" rel="noopener noreferrer" href="%s">documentação</a> cuidadosamente antes de alterar qualquer coisa nesta página.
|
||||
require_db_desc=Gitea requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL).
|
||||
k8s_helper=Se você está rodando o DevStar dentro do Kubernetes, por favor leia a <a target="_blank" rel="noopener noreferrer" href="%s">documentação</a> cuidadosamente antes de alterar qualquer coisa nesta página.
|
||||
require_db_desc=DevStar requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL).
|
||||
db_title=Configurações de banco de dados
|
||||
db_type=Tipo de banco de dados
|
||||
host=Servidor
|
||||
@@ -307,7 +307,7 @@ password_algorithm=Algoritmo Hash de Senha
|
||||
invalid_password_algorithm=Algoritmo de hash de senha inválido
|
||||
password_algorithm_helper=Escolha o algoritmo de hash para as senhas. Diferentes algoritmos têm requerimentos e forças diversos. O algoritmo argon2 é bastante seguro, mas usa muita memória e pode ser inapropriado para sistemas com menos recursos.
|
||||
enable_update_checker=Habilitar Verificador de Atualizações
|
||||
enable_update_checker_helper=Procura por novas versões periodicamente conectando-se ao gitea.io.
|
||||
enable_update_checker_helper=Procura por novas versões periodicamente conectando-se ao devstar.cn.
|
||||
env_config_keys=Configuração do ambiente
|
||||
env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu arquivo de configuração:
|
||||
|
||||
|
||||
@@ -254,8 +254,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Instalação
|
||||
installing_desc=Instalando agora, por favor aguarde...
|
||||
title=Configuração inicial
|
||||
docker_helper=Se correr o Gitea dentro do Docker, leia a <a target="_blank" rel="noopener noreferrer" href="%s">documentação</a> antes de alterar quaisquer configurações.
|
||||
require_db_desc=Gitea requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL).
|
||||
k8s_helper=Se correr o DevStar dentro do Kubernetes, leia a <a target="_blank" rel="noopener noreferrer" href="%s">documentação</a> antes de alterar quaisquer configurações.
|
||||
require_db_desc=DevStar requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL).
|
||||
db_title=Configurações da base de dados
|
||||
db_type=Tipo de base de dados
|
||||
host=Servidor
|
||||
@@ -360,7 +360,7 @@ password_algorithm=Algoritmo de Hash da Senha
|
||||
invalid_password_algorithm=Algoritmo de hash da senha inválido
|
||||
password_algorithm_helper=Definir o algoritmo de hash da senha. Os algoritmos têm requisitos e resistência distintos. `argon2` é bastante seguro, mas usa muita memória e pode ser inapropriado para sistemas pequenos.
|
||||
enable_update_checker=Habilitar verificador de novidades
|
||||
enable_update_checker_helper=Verifica, periodicamente, se foi lançada alguma versão nova, fazendo uma ligação ao gitea.io.
|
||||
enable_update_checker_helper=Verifica, periodicamente, se foi lançada alguma versão nova, fazendo uma ligação ao devstar.cn.
|
||||
env_config_keys=Configuração do ambiente
|
||||
env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu ficheiro de configuração:
|
||||
config_write_file_prompt=Estas opções de configuração serão escritas em: %s
|
||||
|
||||
@@ -201,8 +201,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Установка
|
||||
title=Начальная конфигурация
|
||||
docker_helper=Если вы запускаете Gitea внутри Docker, пожалуйста внимательно прочтите <a target="_blank" rel="noopener noreferrer" href="%s">документацию</a> перед тем, как изменить любые настройки.
|
||||
require_db_desc=Gitea требует MySQL, PostgreSQL, MSSQL, SQLite3 или TiDB (через протокол MySQL).
|
||||
k8s_helper=Если вы запускаете DevStar внутри Kubernetes, пожалуйста внимательно прочтите <a target="_blank" rel="noopener noreferrer" href="%s">документацию</a> перед тем, как изменить любые настройки.
|
||||
require_db_desc=DevStar требует MySQL, PostgreSQL, MSSQL, SQLite3 или TiDB (через протокол MySQL).
|
||||
db_title=Настройки базы данных
|
||||
db_type=Тип базы данных
|
||||
host=Хост
|
||||
@@ -306,7 +306,7 @@ password_algorithm=Алгоритм хеширования пароля
|
||||
invalid_password_algorithm=Некорректный алгоритм хеширования пароля
|
||||
password_algorithm_helper=Задайте алгоритм хеширования паролей. Алгоритмы имеют различные требования и стойкость. Алгоритм argon2 довольно безопасен, но он использует много памяти и может не подходить для слабых систем.
|
||||
enable_update_checker=Включить проверку обновлений
|
||||
enable_update_checker_helper=Периодически проверяет наличие новых версий, подключаясь к gitea.io.
|
||||
enable_update_checker_helper=Периодически проверяет наличие новых версий, подключаясь к devstar.cn.
|
||||
env_config_keys=Настройка окружения
|
||||
env_config_keys_prompt=Следующие переменные окружения также будут применены к вашему конфигурационному файлу:
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=ස්ථාපනය
|
||||
title=මූලික වින්යාසය
|
||||
docker_helper=ඔබ Docker තුළ Gitea ධාවනය කරන්නේ නම්, කරුණාකර ඕනෑම සැකසුම් වෙනස් කිරීමට පෙර <a target="_blank" rel="noopener noreferrer" href="%s">ලියකියවිලි</a> කියවන්න.
|
||||
k8s_helper=ඔබ Kubernetes තුළ DevStar ධාවනය කරන්නේ නම්, කරුණාකර ඕනෑම සැකසුම් වෙනස් කිරීමට පෙර <a target="_blank" rel="noopener noreferrer" href="%s">ලියකියවිලි</a> කියවන්න.
|
||||
db_title=දත්ත සමුදායේ සැකසුම්
|
||||
db_type=දත්ත සමුදායේ වර්ගය
|
||||
host=සත්කාරක
|
||||
|
||||
@@ -200,8 +200,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Inštalácia
|
||||
title=Východzia konfigurácia
|
||||
docker_helper=Ak spúšťate Gitea v Docker kontajneri, prečítajte si <a target="_blank" rel="noopener noreferrer" href="%s">dokumentáciu</a> pred zmenou akýchkoľvek nastavení.
|
||||
require_db_desc=Gitea vyžaduje MySQL, PostgreSQL, MSSQL, SQLite3 alebo TiDB (MySQL protokol).
|
||||
k8s_helper=Ak spúšťate DevStar v Kubernetes kontajneri, prečítajte si <a target="_blank" rel="noopener noreferrer" href="%s">dokumentáciu</a> pred zmenou akýchkoľvek nastavení.
|
||||
require_db_desc=DevStar vyžaduje MySQL, PostgreSQL, MSSQL, SQLite3 alebo TiDB (MySQL protokol).
|
||||
db_title=Nastavenie databázy
|
||||
db_type=Typ databázy
|
||||
host=Host
|
||||
@@ -303,7 +303,7 @@ password_algorithm=Hašovací algoritmus hesla
|
||||
invalid_password_algorithm=Neplatný hash algoritmus hesla
|
||||
password_algorithm_helper=Nastavte algoritmus hashovania hesla. Algoritmy majú rôzne požiadavky a silu. Algoritmus argon2 je pomerne bezpečný, ale využíva veľa pamäte a môže byť nevhodný pre malé systémy.
|
||||
enable_update_checker=Povoliť kontrolu aktualizácií
|
||||
enable_update_checker_helper=Pravidelne kontroluje nové verzie pripojením k gitea.io.
|
||||
enable_update_checker_helper=Pravidelne kontroluje nové verzie pripojením k devstar.cn.
|
||||
|
||||
[home]
|
||||
uname_holder=Používateľské meno alebo emailová adresa
|
||||
|
||||
@@ -124,7 +124,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
[install]
|
||||
install=Installation
|
||||
title=Ursprunglig konfiguration
|
||||
docker_helper=Om du kör Gitea i Docker, vänligen läs igenom <a target="_blank" rel="noopener noreferrer" href="%s">dokumentationen</a> innan några inställningar ändras.
|
||||
k8s_helper=Om du kör DevStar i Kubernetes, vänligen läs igenom <a target="_blank" rel="noopener noreferrer" href="%s">dokumentationen</a> innan några inställningar ändras.
|
||||
db_title=Databasinställningar
|
||||
db_type=Databastyp
|
||||
host=Server
|
||||
|
||||
@@ -251,8 +251,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Kurulum
|
||||
installing_desc=Şimdi kuruluyor, lütfen bekleyin...
|
||||
title=Başlangıç Yapılandırması
|
||||
docker_helper=Eğer Gitea'yı Docker içerisinde çalıştırıyorsanız, lütfen herhangi bir değişiklik yapmadan önce <a target="_blank" rel="noopener noreferrer" href="%s">belgeleri</a> okuyun.
|
||||
require_db_desc=Gitea MySQL, PostgreSQL, MSSQL, SQLite3 veya TiDB (MySQL protokolü) gerektirir.
|
||||
k8s_helper=Eğer DevStar'yı Kubernetes içerisinde çalıştırıyorsanız, lütfen herhangi bir değişiklik yapmadan önce <a target="_blank" rel="noopener noreferrer" href="%s">belgeleri</a> okuyun.
|
||||
require_db_desc=DevStar MySQL, PostgreSQL, MSSQL, SQLite3 veya TiDB (MySQL protokolü) gerektirir.
|
||||
db_title=Veritabanı Ayarları
|
||||
db_type=Veritabanı Türü
|
||||
host=Sunucu
|
||||
@@ -357,7 +357,7 @@ password_algorithm=Parola Hash Algoritması
|
||||
invalid_password_algorithm=Hatalı parola hash algoritması
|
||||
password_algorithm_helper=Parola hash algoritmasını ayarlayın. Algoritmalar değişen gereksinimlere ve güce sahiptirler. argon2 algoritması iyi özelliklere sahip olmasına rağmen fazla miktarda bellek kullanır ve küçük sistemler için uygun olmayabilir.
|
||||
enable_update_checker=Güncelleme Denetleyicisini Etkinleştir
|
||||
enable_update_checker_helper=Düzenli olarak gitea.io'ya bağlanarak yeni yayınlanan sürümleri denetler.
|
||||
enable_update_checker_helper=Düzenli olarak devstar.cn'ya bağlanarak yeni yayınlanan sürümleri denetler.
|
||||
env_config_keys=Ortam Yapılandırma
|
||||
env_config_keys_prompt=Aşağıdaki ortam değişkenleri de yapılandırma dosyanıza eklenecektir:
|
||||
config_write_file_prompt=Bu yapılandırma seçenekleri şuraya yazılacak: %s
|
||||
|
||||
@@ -246,8 +246,8 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
||||
install=Встановлення
|
||||
installing_desc=Встановлення, будь ласка, зачекайте...
|
||||
title=Початкова конфігурація
|
||||
docker_helper=Якщо ви запускаєте Gitea у Docker, будь ласка, прочитайте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a> перед тим, як змінювати будь-які налаштування.
|
||||
require_db_desc=Gitea потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB (протокол MySQL).
|
||||
k8s_helper=Якщо ви запускаєте DevStar у Kubernetes, будь ласка, прочитайте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a> перед тим, як змінювати будь-які налаштування.
|
||||
require_db_desc=DevStar потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB (протокол MySQL).
|
||||
db_title=Налаштування бази даних
|
||||
db_type=Тип бази даних
|
||||
host=Хост
|
||||
@@ -352,7 +352,7 @@ password_algorithm=Алгоритм хешування пароля
|
||||
invalid_password_algorithm=Недійсний хеш-алгоритм пароля
|
||||
password_algorithm_helper=Встановіть алгоритм хешування пароля. Алгоритми мають різні вимоги та стійкість. Алгоритм argon2 є досить безпечним, але використовує багато пам'яті і може бути недоречним для малих систем.
|
||||
enable_update_checker=Увімкнути перевірку оновлень
|
||||
enable_update_checker_helper=Періодично перевіряти наявність нових версій, підключаючись до gitea.io.
|
||||
enable_update_checker_helper=Періодично перевіряти наявність нових версій, підключаючись до devstar.cn.
|
||||
env_config_keys=Конфігурація середовища
|
||||
env_config_keys_prompt=Наступні змінні середовища також будуть застосовані до вашого файлу конфігурації:
|
||||
config_write_file_prompt=Ці параметри будуть записані в: %s
|
||||
|
||||
@@ -251,8 +251,8 @@ license_desc=所有的代码都开源在 <a target="_blank" rel="noopener norefe
|
||||
install=安装页面
|
||||
installing_desc=正在安装,请稍候...
|
||||
title=初始配置
|
||||
docker_helper=如果您正在使用 Docker 容器运行 Gitea,请务必先仔细阅读 <a target="_blank" rel="noopener noreferrer" href="%s">官方文档</a> 后再对本页面进行填写。
|
||||
require_db_desc=Gitea 需要使用 MySQL、PostgreSQL、MSSQL、SQLite3 或 TiDB (MySQL协议) 等数据库
|
||||
k8s_helper=如果您正在使用 Kubernetes 运行 DevStar,请务必先仔细阅读 <a target="_blank" rel="noopener noreferrer" href="%s">官方文档</a> 后再对本页面进行填写。
|
||||
require_db_desc=DevStar 需要使用 MySQL、PostgreSQL、MSSQL、SQLite3 或 TiDB (MySQL协议) 等数据库
|
||||
db_title=数据库设置
|
||||
db_type=数据库类型
|
||||
host=数据库主机
|
||||
@@ -357,7 +357,11 @@ invalid_log_root_path=日志路径无效: %v
|
||||
default_keep_email_private=默认情况下隐藏邮箱地址
|
||||
default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
|
||||
default_allow_create_organization=默认情况下允许创建组织
|
||||
default_allow_create_devcontainer=默认情况下允许创建容器
|
||||
default_allow_create_actrunner=默认情况下允许创建工作流运行器
|
||||
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
|
||||
default_allow_create_devcontainer_popup=默认情况下, 允许新用户帐户创建容器。
|
||||
default_allow_create_actrunner_popup=默认情况下, 允许新用户帐户创建工作流运行器。
|
||||
default_enable_timetracking=默认情况下启用时间跟踪
|
||||
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
|
||||
no_reply_address=隐藏邮件域
|
||||
@@ -366,7 +370,7 @@ password_algorithm=密码哈希算法
|
||||
invalid_password_algorithm=无效的密码哈希算法
|
||||
password_algorithm_helper=设置密码散列算法。算法有不同的要求和强度。 argon2 算法相当安全,但使用大量内存,因此可能不适合小型系统。
|
||||
enable_update_checker=启用更新检查
|
||||
enable_update_checker_helper=通过连接到 gitea.io 定期检查新版本发布。
|
||||
enable_update_checker_helper=通过连接到 devstar.cn 定期检查新版本发布。
|
||||
env_config_keys=环境配置
|
||||
env_config_keys_prompt=以下环境变量也将应用于您的配置文件:
|
||||
config_write_file_prompt=这些配置选项将写入以下位置: %s
|
||||
@@ -3150,6 +3154,7 @@ users.allow_git_hook_tooltip=Git 钩子将会以操作系统用户运行,拥
|
||||
users.allow_import_local=允许导入本地仓库
|
||||
users.allow_create_organization=允许创建组织
|
||||
users.allow_create_devcontainer=允许创建开发容器
|
||||
users.allow_create_actrunner=允许创建工作流运行器
|
||||
users.update_profile=更新帐户
|
||||
users.delete_account=删除帐户
|
||||
users.cannot_delete_self=您不能删除自己
|
||||
@@ -3408,6 +3413,8 @@ config.active_code_lives=激活用户链接有效期
|
||||
config.reset_password_code_lives=恢复账户验证码过期时间
|
||||
config.default_keep_email_private=默认隐藏邮箱地址
|
||||
config.default_allow_create_organization=默认情况下允许创建组织
|
||||
config.default_allow_create_devcontainer=默认情况下允许创建 DevContainer
|
||||
config.default_allow_create_actrunner=默认情况下允许创建 ActRunner
|
||||
config.enable_timetracking=启用时间跟踪
|
||||
config.default_enable_timetracking=默认情况下启用时间跟踪
|
||||
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
||||
@@ -3865,7 +3872,7 @@ status.blocked=阻塞中
|
||||
runners=运行器
|
||||
runners.runner_manage_panel=运行器管理
|
||||
runners.new=创建新运行器
|
||||
runners.new_notice=如何启动一个运行器
|
||||
runners.new_notice=如何手动启动一个运行器
|
||||
runners.status=状态
|
||||
runners.id=ID
|
||||
runners.name=名称
|
||||
@@ -3940,6 +3947,20 @@ workflow.run_success=工作流「%s」已成功运行。
|
||||
workflow.from_ref=使用工作流从
|
||||
workflow.has_workflow_dispatch=此工作流有一个 workflow_dispatch 事件触发器。
|
||||
workflow.has_no_workflow_dispatch=工作流「%s」没有 workflow_dispatch 事件触发器。
|
||||
workflow.debug=调试工作流
|
||||
workflow.content=工作流内容
|
||||
workflow.ref=引用(分支/标签)
|
||||
workflow.event=事件类型
|
||||
workflow.inputs=工作流输入
|
||||
workflow.env=环境变量
|
||||
workflow.logs=执行日志
|
||||
workflow.content_empty=工作流内容不能为空
|
||||
workflow.logs_placeholder=工作流运行时日志将显示在这里...
|
||||
workflow.running=工作流正在运行
|
||||
workflow.input_name=输入名称
|
||||
workflow.input_value=输入值
|
||||
workflow.env_name=环境变量名称
|
||||
workflow.env_value=环境变量值
|
||||
|
||||
need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。
|
||||
|
||||
|
||||
@@ -243,8 +243,8 @@ license_desc=所有的代码都开源在 <a target="_blank" rel="noopener norefe
|
||||
[install]
|
||||
install=安裝頁面
|
||||
title=初始組態
|
||||
docker_helper=如果您在 Docker 中執行 Gitea,請先閱讀<a target="_blank" rel="noopener noreferrer" href="%s">安裝指南</a>再來調整設定。
|
||||
require_db_desc=Gitea 需要 MySQL、PostgreSQL、SQLite3、MSSQL、TiDB (MySQL 協定) 等其中一項。
|
||||
k8s_helper=如果您在 Kubernetes 中執行 DevStar,請先閱讀<a target="_blank" rel="noopener noreferrer" href="%s">安裝指南</a>再來調整設定。
|
||||
require_db_desc=DevStar 需要 MySQL、PostgreSQL、SQLite3、MSSQL、TiDB (MySQL 協定) 等其中一項。
|
||||
db_title=資料庫設定
|
||||
db_type=資料庫類型
|
||||
host=主機
|
||||
@@ -349,7 +349,7 @@ password_algorithm=密碼雜湊演算法
|
||||
invalid_password_algorithm=無效的密碼雜湊演算法
|
||||
password_algorithm_helper=設定密碼雜湊演算法。演算法有不同的需求與強度。argon2 演算法雖然較安全但會使用大量記憶體,可能不適用於小型系統。
|
||||
enable_update_checker=啟用更新檢查器
|
||||
enable_update_checker_helper=定期連線到 gitea.io 檢查更新。
|
||||
enable_update_checker_helper=定期連線到 devstar.cn 檢查更新。
|
||||
env_config_keys=環境組態設定
|
||||
env_config_keys_prompt=下列環境變數也會套用到您的組態檔:
|
||||
config_write_file_prompt=這些配置選項將被寫入到: %s
|
||||
|
||||
@@ -86,12 +86,19 @@ function install {
|
||||
sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION
|
||||
IMAGE_REGISTRY_USER=devstar.cn/devstar
|
||||
fi
|
||||
if sudo docker pull devstar.cn/devstar/webterminal:latest; then
|
||||
success "Successfully pulled devstar.cn/devstar/webterminal:latest"
|
||||
else
|
||||
sudo docker pull mengning997/webterminal:latest
|
||||
success "Successfully pulled mengning997/webterminal:latest renamed to devstar.cn/devstar/webterminal:latest"
|
||||
if sudo docker pull mengning997/webterminal:latest; then
|
||||
sudo docker tag mengning997/webterminal:latest devstar.cn/devstar/webterminal:latest
|
||||
success "Successfully pulled mengning997/webterminal:latest renamed to devstar.cn/devstar/webterminal:latest"
|
||||
else
|
||||
sudo docker pull devstar.cn/devstar/webterminal:latest
|
||||
success "Successfully pulled devstar.cn/devstar/webterminal:latest"
|
||||
fi
|
||||
if sudo docker pull mengning997/act_runner:latest; then
|
||||
sudo docker tag mengning997/act_runner:latest devstar.cn/devstar/act_runner:latest
|
||||
success "Successfully pulled mengning997/act_runner:latest renamed to devstar.cn/devstar/act_runner:latest"
|
||||
else
|
||||
sudo docker pull devstar.cn/devstar/act_runner:latest
|
||||
success "Successfully pulled devstar.cn/devstar/act_runner:latest"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -137,7 +144,13 @@ function stop {
|
||||
fi
|
||||
if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then
|
||||
sudo docker stop devstar-studio && sudo docker rm -f devstar-studio
|
||||
fi
|
||||
fi
|
||||
if [ $(docker ps -a --filter "name=^/webterminal-" -q | wc -l) -gt 0 ]; then
|
||||
sudo docker stop $(docker ps -a --filter "name=^/webterminal-" -q) && sudo docker rm -f $(docker ps -a --filter "name=^/webterminal-" -q)
|
||||
fi
|
||||
if [ $(docker ps -a --filter "name=^/runner-" -q | wc -l) -gt 0 ]; then
|
||||
sudo docker stop $(docker ps -a --filter "name=^/runner-" -q) && sudo docker rm -f $(docker ps -a --filter "name=^/runner-" -q)
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to logs
|
||||
|
||||
1
public/assets/version.json
Normal file
1
public/assets/version.json
Normal file
@@ -0,0 +1 @@
|
||||
{"latest":{"version":"1.25.1"}}
|
||||
@@ -246,6 +246,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
|
||||
AllowCreateDevcontainer: optional.FromPtr(form.AllowCreateDevcontainer),
|
||||
AllowCreateActRunner: optional.FromPtr(form.AllowCreateActRunner),
|
||||
IsRestricted: optional.FromPtr(form.Restricted),
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,8 @@ func Install(ctx *context.Context) {
|
||||
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
||||
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
||||
form.DefaultAllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
||||
form.DefaultAllowCreateActRunner = setting.Service.DefaultAllowCreateActRunner
|
||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||
form.NoReplyAddress = setting.Service.NoReplyAddress
|
||||
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
||||
@@ -490,6 +492,8 @@ func SubmitInstall(ctx *context.Context) {
|
||||
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
|
||||
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
|
||||
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
|
||||
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").SetValue(strconv.FormatBool(form.DefaultAllowCreateDevcontainer))
|
||||
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ACTRUNNER").SetValue(strconv.FormatBool(form.DefaultAllowCreateActRunner))
|
||||
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
||||
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
||||
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
||||
@@ -634,11 +638,17 @@ func SubmitInstall(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 注册Global Runners
|
||||
if setting.Runner.AutoStart {
|
||||
for i := 0; i < setting.Runner.Count; i++ {
|
||||
runners_service.RegistGlobalRunner(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
setting.ClearEnvConfigKeys()
|
||||
log.Info("First-time run install finished!")
|
||||
InstallDone(ctx)
|
||||
|
||||
|
||||
go func() {
|
||||
// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
|
||||
// What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future ....
|
||||
@@ -652,8 +662,6 @@ func SubmitInstall(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
runners_service.RegistGlobalRunner(otherCtx)
|
||||
|
||||
// Now get the http.Server from this request and shut it down
|
||||
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
|
||||
srv := ctx.Value(http.ServerContextKey).(*http.Server)
|
||||
|
||||
@@ -437,6 +437,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
|
||||
AllowCreateDevcontainer: optional.Some(form.AllowCreateDevcontainer),
|
||||
AllowCreateActRunner: optional.Some(form.AllowCreateActRunner),
|
||||
IsRestricted: optional.Some(form.Restricted),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
Language: optional.Some(form.Language),
|
||||
@@ -450,7 +451,6 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
if form.Reset2FA {
|
||||
tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||
|
||||
@@ -55,21 +55,22 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
ctx.Data["ValidateDevContainerConfiguration"] = false
|
||||
}
|
||||
|
||||
ctx.Data["HasDevContainerDockerfile"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
|
||||
ctx.Data["HasDevContainerDockerfile"], ctx.Data["DockerfilePath"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
|
||||
if err != nil {
|
||||
log.Info(err.Error())
|
||||
ctx.Flash.Error(err.Error(), true)
|
||||
}
|
||||
if ctx.Data["HasDevContainer"] == true {
|
||||
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
||||
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
||||
imageName := configurationModel.Image
|
||||
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
||||
log.Info("%v %v", repo, tag)
|
||||
ctx.Data["RepositoryAddress"] = registry
|
||||
ctx.Data["RepositoryUsername"] = namespace
|
||||
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||
|
||||
if ctx.Data["HasDevContainerConfiguration"] == true {
|
||||
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
||||
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
||||
imageName := configurationModel.Image
|
||||
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
||||
log.Info("%v %v", repo, tag)
|
||||
ctx.Data["RepositoryAddress"] = registry
|
||||
ctx.Data["RepositoryUsername"] = namespace
|
||||
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||
}
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
// 获取WebSSH服务端口
|
||||
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
@@ -111,7 +112,6 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
}
|
||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
||||
} else {
|
||||
|
||||
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
|
||||
if err != nil {
|
||||
ctx.Flash.Error(err.Error(), true)
|
||||
@@ -136,7 +136,6 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
|
||||
}
|
||||
|
||||
}
|
||||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||||
if err == nil {
|
||||
@@ -145,7 +144,6 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 携带数据渲染页面,返回
|
||||
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
||||
ctx.Data["PageIsDevContainer"] = true
|
||||
@@ -300,7 +298,7 @@ func UpdateDevContainer(ctx *context.Context) {
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo.Repository, &updateInfo)
|
||||
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo, &updateInfo)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||
return
|
||||
@@ -318,18 +316,43 @@ func GetTerminalCommand(ctx *context.Context) {
|
||||
log.Info(err.Error())
|
||||
status = "error"
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status})
|
||||
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status, "workdir": "/workspace/" + ctx.Repo.Repository.Name})
|
||||
}
|
||||
|
||||
func GetDevContainerOutput(ctx *context.Context) {
|
||||
// 设置 CORS 响应头
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
query := ctx.Req.URL.Query()
|
||||
output, err := devcontainer_service.GetDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
log.Info(err.Error())
|
||||
}
|
||||
ctx.JSON(http.StatusOK, output)
|
||||
ctx.JSON(http.StatusOK, map[string]string{"output": output})
|
||||
}
|
||||
func SaveDevContainerOutput(ctx *context.Context) {
|
||||
// 设置 CORS 响应头
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
// 处理 OPTIONS 预检请求
|
||||
if ctx.Req.Method == "OPTIONS" {
|
||||
ctx.JSON(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
|
||||
query := ctx.Req.URL.Query()
|
||||
|
||||
// 从请求体中读取输出内容
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
log.Error("Failed to read request body: %v", err)
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read request body"})
|
||||
return
|
||||
}
|
||||
err = devcontainer_service.SaveDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository, string(body))
|
||||
if err != nil {
|
||||
log.Info(err.Error())
|
||||
}
|
||||
ctx.JSON(http.StatusOK, "")
|
||||
}
|
||||
|
||||
384
routers/web/repo/actions/debug.go
Normal file
384
routers/web/repo/actions/debug.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
context_module "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Debug shows the debug page
|
||||
func Debug(ctx *context_module.Context) {
|
||||
ctx.Data["PageIsActions"] = true
|
||||
|
||||
workflowID := ctx.FormString("workflow")
|
||||
if workflowID == "" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get workflow content
|
||||
content, err := actions_service.GetDebugSessionWorkflowContent(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDebugSessionWorkflowContent", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new debug session
|
||||
debugSession := &actions_model.DebugSession{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
WorkflowID: workflowID,
|
||||
WorkflowContent: content,
|
||||
}
|
||||
|
||||
if err := actions_model.CreateDebugSession(ctx, debugSession); err != nil {
|
||||
ctx.ServerError("CreateDebugSession", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["DebugSessionID"] = debugSession.ID
|
||||
ctx.Data["WorkflowID"] = workflowID
|
||||
ctx.Data["WorkflowContent"] = content
|
||||
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
|
||||
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
|
||||
ctx.Data["DebugAPIURL"] = ctx.Repo.RepoLink + "/actions/debug-api"
|
||||
|
||||
ctx.HTML(http.StatusOK, "repo/actions/debug")
|
||||
}
|
||||
|
||||
// DebugRunRequest represents the request to run a debug workflow
|
||||
type DebugRunRequest struct {
|
||||
WorkflowContent string `json:"workflow_content"`
|
||||
Ref string `json:"ref"`
|
||||
Event string `json:"event"`
|
||||
Inputs map[string]string `json:"inputs"`
|
||||
Env map[string]string `json:"env"`
|
||||
}
|
||||
|
||||
// DebugRunResponse represents the response of running a debug workflow
|
||||
type DebugRunResponse struct {
|
||||
RunIndex int64 `json:"run_index"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// APIDebugRun handles running a debug workflow
|
||||
func APIDebugRun(ctx *context_module.Context) {
|
||||
log.Info("APIDebugRun called")
|
||||
debugSessionID := ctx.PathParamInt64("debugSessionID")
|
||||
log.Info("Debug session ID: %d", debugSessionID)
|
||||
|
||||
// Verify the debug session belongs to this repo
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
log.Error("GetDebugSession error: %v", err)
|
||||
ctx.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "debug session not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RepoID != ctx.Repo.Repository.ID {
|
||||
log.Error("Repo mismatch: debug session repo %d, current repo %d", debugSession.RepoID, ctx.Repo.Repository.ID)
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"error": "forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req DebugRunRequest
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||
log.Error("JSON decode error: %v", err)
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid request body: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Received debug run request: content_len=%d, ref=%s, event=%s", len(req.WorkflowContent), req.Ref, req.Event)
|
||||
|
||||
if req.Inputs == nil {
|
||||
req.Inputs = make(map[string]string)
|
||||
}
|
||||
if req.Env == nil {
|
||||
req.Env = make(map[string]string)
|
||||
}
|
||||
|
||||
if req.WorkflowContent == "" {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "workflow content is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Event == "" {
|
||||
req.Event = "push"
|
||||
}
|
||||
|
||||
// Run debug workflow
|
||||
runIndex, err := actions_service.DebugWorkflow(ctx, ctx.Doer, ctx.Repo.Repository,
|
||||
ctx.Repo.GitRepo, debugSessionID, req.WorkflowContent, req.Ref,
|
||||
req.Event, req.Inputs, req.Env)
|
||||
|
||||
if err != nil {
|
||||
log.Error("DebugWorkflow error: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": fmt.Sprintf("failed to run debug workflow: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Debug workflow started successfully: run_index=%d", runIndex)
|
||||
ctx.JSON(http.StatusOK, DebugRunResponse{
|
||||
RunIndex: runIndex,
|
||||
Success: true,
|
||||
Message: "Debug workflow started",
|
||||
})
|
||||
}
|
||||
|
||||
// APIDebugSession returns the debug session status
|
||||
func APIDebugSession(ctx *context_module.Context) {
|
||||
debugSessionID := ctx.PathParamInt64("debugSessionID")
|
||||
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "debug session not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"error": "forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"id": debugSession.ID,
|
||||
"workflow_id": debugSession.WorkflowID,
|
||||
"status": debugSession.Status,
|
||||
"run_id": debugSession.RunID,
|
||||
"workflow_content": debugSession.WorkflowContent,
|
||||
"error_msg": debugSession.ErrorMsg,
|
||||
"created_unix": debugSession.CreatedUnix,
|
||||
"updated_unix": debugSession.UpdatedUnix,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// APIDebugSessionUpdate updates the debug session workflow content
|
||||
func APIDebugSessionUpdate(ctx *context_module.Context) {
|
||||
debugSessionID := ctx.PathParamInt64("debugSessionID")
|
||||
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "debug session not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"error": "forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.Status != actions_model.DebugSessionStatusDraft {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "debug session is not in draft status",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
content := ctx.FormString("workflow_content")
|
||||
if content == "" {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "workflow content is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
debugSession.WorkflowContent = content
|
||||
if err := actions_model.UpdateDebugSession(ctx, debugSession); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to update debug session",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "debug session updated",
|
||||
})
|
||||
}
|
||||
|
||||
// APIDebugSessionDelete deletes a debug session
|
||||
func APIDebugSessionDelete(ctx *context_module.Context) {
|
||||
debugSessionID := ctx.PathParamInt64("debugSessionID")
|
||||
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "debug session not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"error": "forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_model.DeleteDebugSession(ctx, debugSessionID); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to delete debug session",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "debug session deleted",
|
||||
})
|
||||
}
|
||||
|
||||
// APIDebugLogs returns the logs for a debug workflow run
|
||||
func APIDebugLogs(ctx *context_module.Context) {
|
||||
debugSessionID := ctx.PathParamInt64("debugSessionID")
|
||||
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, map[string]string{
|
||||
"error": "debug session not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"error": "forbidden",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugSession.RunID == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "not_started",
|
||||
"logs": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the run using GetRunByRepoAndID
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, debugSession.RepoID, debugSession.RunID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to get run",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get jobs for this run
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to get jobs",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": run.Status.String(),
|
||||
"logs": "No jobs found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Collect logs from all jobs
|
||||
var allLogs string
|
||||
for _, job := range jobs {
|
||||
allLogs += fmt.Sprintf("=== Job: %s (Status: %s) ===\n", job.Name, job.Status.String())
|
||||
|
||||
if job.TaskID > 0 {
|
||||
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get task %d: %v", job.TaskID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
task.Job = job
|
||||
if err := task.LoadAttributes(ctx); err != nil {
|
||||
log.Error("Failed to load task attributes: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get logs for this task
|
||||
jobLogs, err := getTaskLogs(ctx, task)
|
||||
if err != nil {
|
||||
log.Error("Failed to get task logs: %v", err)
|
||||
allLogs += fmt.Sprintf("Error getting logs: %v\n", err)
|
||||
} else {
|
||||
allLogs += jobLogs
|
||||
}
|
||||
} else {
|
||||
allLogs += "Task not started yet\n"
|
||||
}
|
||||
allLogs += "\n"
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": run.Status.String(),
|
||||
"logs": allLogs,
|
||||
"run_index": run.Index,
|
||||
"run_link": run.Link(),
|
||||
})
|
||||
}
|
||||
|
||||
// getTaskLogs retrieves all logs for a task
|
||||
func getTaskLogs(ctx *context_module.Context, task *actions_model.ActionTask) (string, error) {
|
||||
if task.LogExpired {
|
||||
return "Logs have expired\n", nil
|
||||
}
|
||||
|
||||
steps := actions.FullSteps(task)
|
||||
var allLogs string
|
||||
|
||||
for i, step := range steps {
|
||||
allLogs += fmt.Sprintf("\n--- Step %d: %s (Status: %s) ---\n", i+1, step.Name, step.Status.String())
|
||||
|
||||
if step.LogLength == 0 {
|
||||
allLogs += "(No output)\n"
|
||||
continue
|
||||
}
|
||||
|
||||
// Read logs for this step
|
||||
offset := task.LogIndexes[step.LogIndex]
|
||||
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, step.LogLength)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read logs: %w", err)
|
||||
}
|
||||
|
||||
for _, row := range logRows {
|
||||
allLogs += row.Content + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return allLogs, nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
@@ -411,6 +413,23 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
log.Info("File deleted: %s", treePath)
|
||||
if treePath == `.devcontainer/devcontainer.json` {
|
||||
var userIds []int64
|
||||
err = db.GetEngine(ctx).
|
||||
Table("devcontainer").
|
||||
Select("user_id").
|
||||
Where("repo_id = ?", ctx.Repo.Repository.ID).
|
||||
Find(&userIds)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetEngine", err)
|
||||
return
|
||||
}
|
||||
for _, userId := range userIds {
|
||||
devcontainer_service.DeleteDevContainer(ctx, userId, ctx.Repo.Repository.ID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||
|
||||
@@ -160,6 +160,7 @@ func Runners(ctx *context.Context) {
|
||||
ctx.Data["RunnerOwnerID"] = opts.OwnerID
|
||||
ctx.Data["RunnerRepoID"] = opts.RepoID
|
||||
ctx.Data["SortType"] = opts.Sort
|
||||
ctx.Data["AllowCreateActRunner"] = ctx.Doer.AllowCreateActRunner
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
|
||||
@@ -300,6 +301,14 @@ func RegisterARunner(ctx *context.Context) {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否有权创建 runner
|
||||
if !ctx.Doer.AllowCreateActRunner {
|
||||
ctx.Flash.Error(ctx.Tr("actions.runners.create_runner_permission_denied"))
|
||||
ctx.Redirect(rCtx.RedirectLink)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := actions_model.NewRunnerToken(ctx, rCtx.OwnerID, rCtx.RepoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("NewRunnerToken", err)
|
||||
|
||||
@@ -1434,13 +1434,14 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Get("/status", devcontainer_web.GetDevContainerStatus)
|
||||
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
||||
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
||||
m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
|
||||
},
|
||||
// 解析仓库信息
|
||||
// 具有code读取权限
|
||||
context.RepoAssignment, reqUnitCodeReader,
|
||||
)
|
||||
m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
|
||||
m.Get("/vscode-home", devcontainer_web.VscodeHome)
|
||||
m.Get("/vscode-home", devcontainer_web.VscodeHome)
|
||||
m.Group("/api/devcontainer", func() {
|
||||
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
||||
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
||||
@@ -1542,6 +1543,14 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
||||
m.Post("/run", reqRepoActionsWriter, actions.Run)
|
||||
m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs)
|
||||
m.Get("/debug", reqRepoActionsWriter, actions.Debug)
|
||||
m.Group("/debug-api/{debugSessionID}", func() {
|
||||
m.Post("/run", reqRepoActionsWriter, actions.APIDebugRun)
|
||||
m.Get("/session", actions.APIDebugSession)
|
||||
m.Get("/logs", actions.APIDebugLogs)
|
||||
m.Post("/update", reqRepoActionsWriter, actions.APIDebugSessionUpdate)
|
||||
m.Post("/delete", reqRepoActionsWriter, actions.APIDebugSessionDelete)
|
||||
})
|
||||
|
||||
m.Group("/runs/{run}", func() {
|
||||
m.Combo("").
|
||||
|
||||
173
services/actions/debug_workflow.go
Normal file
173
services/actions/debug_workflow.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
)
|
||||
|
||||
// DebugWorkflow starts a debug run for a workflow
|
||||
func DebugWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository,
|
||||
gitRepo *git.Repository, debugSessionID int64, workflowContent string, ref string,
|
||||
event string, inputs map[string]string, env map[string]string) (int64, error) {
|
||||
|
||||
if workflowContent == "" {
|
||||
return 0, fmt.Errorf("workflow content is empty")
|
||||
}
|
||||
|
||||
if ref == "" {
|
||||
ref = repo.DefaultBranch
|
||||
}
|
||||
|
||||
if event == "" {
|
||||
event = "push"
|
||||
}
|
||||
|
||||
// Get target commit from ref
|
||||
refName := git.RefName(ref)
|
||||
var runTargetCommit *git.Commit
|
||||
var err error
|
||||
|
||||
if refName.IsTag() {
|
||||
runTargetCommit, err = gitRepo.GetTagCommit(refName.TagName())
|
||||
} else if refName.IsBranch() {
|
||||
runTargetCommit, err = gitRepo.GetBranchCommit(refName.BranchName())
|
||||
} else {
|
||||
refName = git.RefNameFromBranch(ref)
|
||||
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get commit: %w", err)
|
||||
}
|
||||
|
||||
// Parse workflow from content
|
||||
workflows, err := jobparser.Parse([]byte(workflowContent))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse workflow: %w", err)
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
return 0, fmt.Errorf("no workflows found in content")
|
||||
}
|
||||
|
||||
// Create action run
|
||||
run := &actions_model.ActionRun{
|
||||
Title: fmt.Sprintf("Debug: %s", workflows[0].Name),
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: fmt.Sprintf("debug_%d", debugSessionID),
|
||||
TriggerUserID: doer.ID,
|
||||
TriggerUser: doer,
|
||||
Ref: string(refName),
|
||||
CommitSHA: runTargetCommit.ID.String(),
|
||||
IsForkPullRequest: false,
|
||||
Event: webhook_module.HookEventType(event),
|
||||
TriggerEvent: event,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
|
||||
// Build event payload
|
||||
eventPayload := map[string]interface{}{
|
||||
"inputs": inputs,
|
||||
"ref": ref,
|
||||
}
|
||||
|
||||
// Convert to API payload
|
||||
workflowDispatchPayload := &api.WorkflowDispatchPayload{
|
||||
Workflow: fmt.Sprintf("debug_%d", debugSessionID),
|
||||
Ref: ref,
|
||||
Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
||||
Inputs: eventPayload,
|
||||
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
|
||||
}
|
||||
|
||||
var eventPayloadBytes []byte
|
||||
if eventPayloadBytes, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
||||
return 0, fmt.Errorf("JSONPayload: %w", err)
|
||||
}
|
||||
|
||||
run.EventPayload = string(eventPayloadBytes)
|
||||
|
||||
// Insert the action run and its associated jobs into the database
|
||||
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
||||
return 0, fmt.Errorf("InsertRun: %w", err)
|
||||
}
|
||||
|
||||
// Update debug session with run ID
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err == nil && debugSession != nil {
|
||||
debugSession.RunID = run.ID
|
||||
debugSession.Status = actions_model.DebugSessionStatusRunning
|
||||
_ = actions_model.UpdateDebugSession(ctx, debugSession)
|
||||
}
|
||||
|
||||
// Trigger notification to start the run
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||
|
||||
log.Info("Debug workflow started: run_id=%d, debug_session_id=%d", run.ID, debugSessionID)
|
||||
|
||||
return run.Index, nil
|
||||
}
|
||||
|
||||
// GetDebugWorkflowStatus returns the status of a debug workflow run
|
||||
func GetDebugWorkflowStatus(ctx reqctx.RequestContext, debugSessionID int64) (*actions_model.ActionRun, error) {
|
||||
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if debugSession.RunID == 0 {
|
||||
return nil, nil // Not run yet
|
||||
}
|
||||
|
||||
return actions_model.GetRunByRepoAndID(ctx, debugSession.RepoID, debugSession.RunID)
|
||||
}
|
||||
|
||||
// GetDebugSessionWorkflowContent retrieves the workflow content from repository for a debug session
|
||||
func GetDebugSessionWorkflowContent(ctx reqctx.RequestContext, repo *repo_model.Repository, gitRepo *git.Repository,
|
||||
workflowID string) (string, error) {
|
||||
|
||||
// Get default branch commit
|
||||
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// List workflows
|
||||
_, entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Find the workflow file
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == workflowID {
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("workflow %q not found", workflowID)
|
||||
}
|
||||
@@ -399,6 +399,8 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
ctx.Data["Permission"] = &ctx.Repo.Permission
|
||||
if ctx.Doer != nil {
|
||||
ctx.Data["AllowCreateDevcontainer"] = ctx.Doer.AllowCreateDevcontainer
|
||||
ctx.Data["AllowCreateActRunner"] = ctx.Doer.AllowCreateActRunner
|
||||
|
||||
} else {
|
||||
query := ctx.Req.URL.Query()
|
||||
userID := query.Get("user")
|
||||
@@ -416,6 +418,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
return
|
||||
}
|
||||
ctx.Data["AllowCreateDevcontainer"] = u.AllowCreateDevcontainer
|
||||
ctx.Data["AllowCreateActRunner"] = u.AllowCreateActRunner
|
||||
}
|
||||
|
||||
if repo.IsMirror {
|
||||
|
||||
@@ -150,7 +150,7 @@ func registerUpdateGiteaChecker() {
|
||||
RunAtStart: false,
|
||||
Schedule: "@every 168h",
|
||||
},
|
||||
HTTPEndpoint: "https://dl.gitea.com/gitea/version.json",
|
||||
HTTPEndpoint: "https://devstar.cn/assets/version.json",
|
||||
}, func(ctx context.Context, _ *user_model.User, config Config) error {
|
||||
updateCheckerConfig := config.(*UpdateCheckerConfig)
|
||||
return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint)
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -70,21 +68,21 @@ func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repos
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
|
||||
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, string, error) {
|
||||
_, err := FileExists(".devcontainer/devcontainer.json", repo)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
return false, nil
|
||||
return false, "", nil
|
||||
}
|
||||
return false, err
|
||||
return false, "", err
|
||||
}
|
||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", err
|
||||
}
|
||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", err
|
||||
}
|
||||
// 执行验证
|
||||
if errs := configurationModel.Validate(); len(errs) > 0 {
|
||||
@@ -92,20 +90,34 @@ func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Reposito
|
||||
for _, err := range errs {
|
||||
fmt.Printf(" - %s\n", err.Error())
|
||||
}
|
||||
return false, fmt.Errorf("配置格式错误")
|
||||
return false, "", fmt.Errorf("配置格式错误")
|
||||
} else {
|
||||
log.Info("%v", configurationModel)
|
||||
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
||||
return false, nil
|
||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
return false, "", nil
|
||||
}
|
||||
return false, "", err
|
||||
}
|
||||
return true, ".devcontainer/Dockerfile", nil
|
||||
}
|
||||
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
return false, nil
|
||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
return false, "", nil
|
||||
}
|
||||
return false, "", err
|
||||
}
|
||||
return true, ".devcontainer/Dockerfile", nil
|
||||
}
|
||||
return false, err
|
||||
return false, "", err
|
||||
}
|
||||
return true, nil
|
||||
return true, ".devcontainer/" + configurationModel.Build.Dockerfile, nil
|
||||
}
|
||||
}
|
||||
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
|
||||
@@ -435,7 +447,7 @@ func StopDevContainer(ctx context.Context, userID, repoID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error {
|
||||
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_context.Repository, updateInfo *UpdateInfo) error {
|
||||
dbEngine := db.GetEngine(ctx)
|
||||
var devContainerInfo devcontainer_models.Devcontainer
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
@@ -445,25 +457,24 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit
|
||||
_, err = dbEngine.
|
||||
Table("devcontainer").
|
||||
Select("*").
|
||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID).
|
||||
Get(&devContainerInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
otherCtx := context.Background()
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
//k8s的逻辑
|
||||
} else {
|
||||
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -534,58 +545,72 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
//正在创建容器,创建容器成功,则状态转移
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
//k8s的逻辑
|
||||
|
||||
} else {
|
||||
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
||||
exist, _, err := ContainerExists(ctx, devContainerInfo.Name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if status == "created" {
|
||||
//添加脚本文件
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
} else {
|
||||
userNum, err := strconv.ParseInt(userID, 10, 64)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var scriptContent string
|
||||
scriptContent, err = GetCommandContent(ctx, userNum, repo)
|
||||
log.Info("command: %s", scriptContent)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 创建 tar 归档文件
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
defer tw.Close()
|
||||
|
||||
// 添加文件到 tar 归档
|
||||
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
|
||||
// 创建 Docker 客户端
|
||||
cli, err := docker_module.CreateDockerClient(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 获取容器 ID
|
||||
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
|
||||
if err != nil {
|
||||
log.Info("%v", err)
|
||||
return "", "", err
|
||||
}
|
||||
if !exist {
|
||||
_, err = dbEngine.Table("devcontainer_output").
|
||||
Select("command").
|
||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
|
||||
Get(&cmd)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
realTimeStatus = 3
|
||||
} else {
|
||||
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if status == "created" {
|
||||
//添加脚本文件
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
} else {
|
||||
userNum, err := strconv.ParseInt(userID, 10, 64)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var scriptContent string
|
||||
scriptContent, err = GetCommandContent(ctx, userNum, repo)
|
||||
log.Info("command: %s", scriptContent)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 创建 tar 归档文件
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
defer tw.Close()
|
||||
// 添加文件到 tar 归档
|
||||
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
|
||||
// 创建 Docker 客户端
|
||||
cli, err := docker_module.CreateDockerClient(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 获取容器 ID
|
||||
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
|
||||
if err != nil {
|
||||
log.Info("%v", err)
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
realTimeStatus = 3
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
break
|
||||
case 3:
|
||||
@@ -614,6 +639,27 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
postAttachCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostAttachCommand), "\n"))
|
||||
if _, ok := configurationModel.PostAttachCommand.(map[string]interface{}); ok {
|
||||
// 是 map[string]interface{} 类型
|
||||
cmdObj := configurationModel.PostAttachCommand.(map[string]interface{})
|
||||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
||||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
postAttachCommand += "\n" + fileCommand
|
||||
}
|
||||
}
|
||||
cmd += postAttachCommand
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -636,67 +682,59 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
||||
}
|
||||
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
||||
}
|
||||
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
|
||||
var devContainerOutput []devcontainer_models.DevcontainerOutput
|
||||
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
|
||||
var devContainerOutput string
|
||||
dbEngine := db.GetEngine(ctx)
|
||||
resp := OutputResponse{}
|
||||
var status string
|
||||
var containerName string
|
||||
_, err := dbEngine.
|
||||
Table("devcontainer").
|
||||
Select("devcontainer_status, name").
|
||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||
Get(&status, &containerName)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = dbEngine.Table("devcontainer_output").
|
||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
||||
Find(&devContainerOutput)
|
||||
_, err := dbEngine.Table("devcontainer_output").
|
||||
Select("output").
|
||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||
Get(&devContainerOutput)
|
||||
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(devContainerOutput) > 0 {
|
||||
|
||||
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
|
||||
resp.CurrentJob.Detail = status
|
||||
if status == "4" {
|
||||
// 获取WebSSH服务端口
|
||||
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
|
||||
if err == nil {
|
||||
return resp, err
|
||||
}
|
||||
// 解析URL
|
||||
u, err := url.Parse(webTerminalURL)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
// 分离主机和端口
|
||||
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
|
||||
resp.CurrentJob.IP = terminalHost
|
||||
resp.CurrentJob.Port = terminalPort
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
for _, item := range devContainerOutput {
|
||||
logLines := []ViewStepLogLine{}
|
||||
logLines = append(logLines, ViewStepLogLine{
|
||||
Index: 1,
|
||||
Message: item.Output,
|
||||
if devContainerOutput != "" {
|
||||
_, err = dbEngine.Table("devcontainer_output").
|
||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||
Update(map[string]interface{}{
|
||||
"output": "",
|
||||
})
|
||||
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
|
||||
Summary: item.Command,
|
||||
Status: item.Status,
|
||||
Logs: logLines,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
return devContainerOutput, nil
|
||||
}
|
||||
func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error {
|
||||
var devContainerOutput string
|
||||
var finalOutput string
|
||||
dbEngine := db.GetEngine(ctx)
|
||||
|
||||
// 从数据库中获取现有的输出内容
|
||||
_, err := dbEngine.Table("devcontainer_output").
|
||||
Select("output").
|
||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||
Get(&devContainerOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n")
|
||||
if newoutput == "\b \b" {
|
||||
finalOutput = devContainerOutput[:len(devContainerOutput)-1]
|
||||
} else {
|
||||
finalOutput = devContainerOutput + newoutput
|
||||
}
|
||||
_, err = dbEngine.Table("devcontainer_output").
|
||||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||
Update(map[string]interface{}{
|
||||
"output": finalOutput + "\r\n",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
@@ -937,7 +975,6 @@ func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository)
|
||||
script = append(script, v)
|
||||
}
|
||||
scriptCommand := strings.TrimSpace(strings.Join(script, "\n"))
|
||||
|
||||
userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n"
|
||||
assetFS := templates.AssetFS()
|
||||
Content_tmpl, err := assetFS.ReadFile("repo/devcontainer/devcontainer_tmpl.sh")
|
||||
@@ -989,6 +1026,7 @@ func AddPublicKeyToAllRunningDevContainer(ctx context.Context, userId int64, pub
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(devcontainerList) > 0 {
|
||||
// 将公钥写入这些打开的容器中
|
||||
for _, repoDevContainer := range devcontainerList {
|
||||
|
||||
@@ -16,10 +16,13 @@ import (
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
docker_module "code.gitea.io/gitea/modules/docker"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/go-connections/nat"
|
||||
@@ -129,6 +132,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var imageName = configurationModel.Image
|
||||
dockerSocket, err := docker_module.GetDockerSocketPath()
|
||||
if err != nil {
|
||||
@@ -213,7 +217,8 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
||||
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
|
||||
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
|
||||
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` +
|
||||
` -e DEVCONTAINER_STATUS="start" `
|
||||
` -e DEVCONTAINER_STATUS="start" ` +
|
||||
` -e WEB_TERMINAL_HELLO="Successfully connected to the devcontainer" `
|
||||
// 遍历 ContainerEnv 映射中的每个环境变量
|
||||
for name, value := range configurationModel.ContainerEnv {
|
||||
// 将每个环境变量转换为 "-e name=value" 格式
|
||||
@@ -283,7 +288,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
||||
Status: "waitting",
|
||||
UserId: newDevcontainer.UserId,
|
||||
RepoId: newDevcontainer.RepoId,
|
||||
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo 'Successfully connected to the container';bash"` + "\n",
|
||||
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c 'echo "$WEB_TERMINAL_HELLO";bash'` + "\n",
|
||||
ListId: 4,
|
||||
DevcontainerId: newDevcontainer.Id,
|
||||
}); err != nil {
|
||||
@@ -391,17 +396,16 @@ func StopDevContainerByDocker(ctx context.Context, devContainerName string) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *repo.Repository, doer *user.User) error {
|
||||
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *gitea_context.Repository, doer *user.User) error {
|
||||
// 创建docker client
|
||||
cli, err := docker_module.CreateDockerClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
// update容器
|
||||
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -411,16 +415,45 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
||||
}
|
||||
|
||||
if updateInfo.SaveMethod == "on" {
|
||||
|
||||
// 创建构建上下文(包含Dockerfile的tar包)
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
defer tw.Close()
|
||||
// 添加Dockerfile到tar包
|
||||
var dockerfileContent string
|
||||
dockerfile := "Dockerfile"
|
||||
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
|
||||
if err != nil {
|
||||
return err
|
||||
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/"+configurationModel.Build.Dockerfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content := []byte(dockerfileContent)
|
||||
header := &tar.Header{
|
||||
Name: dockerfile,
|
||||
@@ -468,11 +501,12 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 定义正则表达式来匹配 image 字段
|
||||
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
|
||||
// 使用正则表达式查找并替换 image 字段的值
|
||||
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
|
||||
err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer)
|
||||
err = UpdateDevcontainerConfiguration(newConfiguration, repo.Repository, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -484,7 +518,6 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
||||
// - bool: 镜像是否存在(true=存在,false=不存在)
|
||||
// - error: 非空表示检查过程中发生错误
|
||||
func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
||||
|
||||
// 创建 Docker 客户端
|
||||
cli, err := docker_module.CreateDockerClient(ctx)
|
||||
if err != nil {
|
||||
@@ -519,7 +552,6 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
// 创建 exec 实例
|
||||
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
||||
if err != nil {
|
||||
@@ -542,6 +574,7 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string
|
||||
exitCode = resp.ExitCode
|
||||
return exitCode == 0, nil // 退出码为 0 表示目录存在
|
||||
}
|
||||
|
||||
func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) {
|
||||
// 上下文
|
||||
// 创建 Docker 客户端
|
||||
@@ -598,7 +631,7 @@ func RegistWebTerminal(ctx context.Context) error {
|
||||
// 拉取镜像
|
||||
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
||||
fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
@@ -632,3 +665,36 @@ func RegistWebTerminal(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContainerExists 检查容器是否存在,返回存在状态和容器ID(如果存在)
|
||||
func ContainerExists(ctx context.Context, containerName string) (bool, string, error) {
|
||||
cli, err := docker_module.CreateDockerClient(ctx)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
// 设置过滤器,根据容器名称过滤
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("name", containerName)
|
||||
|
||||
// 获取容器列表,使用过滤器
|
||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||
All: true, // 包括所有容器(运行的和停止的)
|
||||
Filters: filter,
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// 遍历容器,检查名称是否完全匹配
|
||||
for _, container := range containers {
|
||||
for _, name := range container.Names {
|
||||
// 容器名称在Docker API中是以斜杠开头的,例如 "/my-container"
|
||||
// 所以我们需要检查去掉斜杠后的名称是否匹配
|
||||
if strings.TrimPrefix(name, "/") == containerName {
|
||||
return true, container.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
@@ -48,8 +48,9 @@ type AdminEditUserForm struct {
|
||||
Restricted bool
|
||||
AllowGitHook bool
|
||||
AllowImportLocal bool
|
||||
AllowCreateOrganization bool
|
||||
AllowCreateDevcontainer bool
|
||||
AllowCreateOrganization bool `form:"allow_create_organization"`
|
||||
AllowCreateDevcontainer bool `form:"allow_create_devcontainer"`
|
||||
AllowCreateActRunner bool `form:"allow_create_actrunner"`
|
||||
ProhibitLogin bool
|
||||
Reset2FA bool `form:"reset_2fa"`
|
||||
Visibility structs.VisibleType
|
||||
|
||||
@@ -60,7 +60,9 @@ type InstallForm struct {
|
||||
EnableCaptcha bool
|
||||
RequireSignInView bool
|
||||
DefaultKeepEmailPrivate bool
|
||||
DefaultAllowCreateOrganization bool
|
||||
DefaultAllowCreateOrganization bool `form:"default_allow_create_organization"`
|
||||
DefaultAllowCreateDevcontainer bool `form:"default_allow_create_devcontainer"`
|
||||
DefaultAllowCreateActRunner bool `form:"default_allow_create_actrunner"`
|
||||
DefaultEnableTimetracking bool
|
||||
EnableUpdateChecker bool
|
||||
NoReplyAddress string
|
||||
|
||||
@@ -40,6 +40,7 @@ func checkK8sIsEnable() bool {
|
||||
|
||||
func RegistRunner(ctx context.Context, token string) error {
|
||||
log.Info("开始注册Runner...")
|
||||
|
||||
var err error
|
||||
if checkK8sIsEnable() {
|
||||
err = registK8sRunner(ctx, token)
|
||||
|
||||
@@ -52,6 +52,7 @@ type UpdateOptions struct {
|
||||
DiffViewStyle optional.Option[string]
|
||||
AllowCreateOrganization optional.Option[bool]
|
||||
AllowCreateDevcontainer optional.Option[bool]
|
||||
AllowCreateActRunner optional.Option[bool]
|
||||
IsActive optional.Option[bool]
|
||||
IsAdmin optional.Option[UpdateOptionField[bool]]
|
||||
EmailNotificationsPreference optional.Option[string]
|
||||
@@ -170,6 +171,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
|
||||
cols = append(cols, "allow_create_devcontainer")
|
||||
}
|
||||
if opts.AllowCreateActRunner.Has() {
|
||||
u.AllowCreateActRunner = opts.AllowCreateActRunner.Value()
|
||||
|
||||
cols = append(cols, "allow_create_act_runner")
|
||||
}
|
||||
if opts.RepoAdminChangeTeamAccess.Has() {
|
||||
u.RepoAdminChangeTeamAccess = opts.RepoAdminChangeTeamAccess.Value()
|
||||
|
||||
|
||||
@@ -153,6 +153,10 @@
|
||||
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
||||
<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_devcontainer"}}</dt>
|
||||
<dd>{{svg (Iif .Service.DefaultAllowCreateDevcontainer "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_actrunner"}}</dt>
|
||||
<dd>{{svg (Iif .Service.DefaultAllowCreateActRunner "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
|
||||
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
|
||||
{{if .Service.EnableTimetracking}}
|
||||
|
||||
@@ -155,6 +155,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{ctx.Locale.Tr "admin.users.allow_create_actrunner"}}</strong></label>
|
||||
<input name="allow_create_actrunner" type="checkbox" {{if .User.AllowCreateActRunner}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .TwoFactorEnabled}}
|
||||
<div class="divider"></div>
|
||||
<div class="inline field">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
|
||||
<p>{{ctx.Locale.Tr "install.k8s_helper" "https://www.mengning.com/src/devstar/install/install-k8s"}}</p>
|
||||
|
||||
<form class="ui form" action="{{AppSubUrl}}/" method="post">
|
||||
<!-- Database Settings -->
|
||||
@@ -160,7 +160,7 @@
|
||||
<div>
|
||||
|
||||
<!-- k8s -->
|
||||
<details class="optional field">
|
||||
<details class="optional field">
|
||||
<summary class="right-content tw-py-2{{if .Err_K8s}} text red{{end}}">
|
||||
{{ctx.Locale.Tr "install.k8s_title"}}
|
||||
</summary>
|
||||
@@ -178,7 +178,6 @@
|
||||
<label for="k8s_token">{{ctx.Locale.Tr "install.k8s_token"}}</label>
|
||||
<input id="k8s_token" name="k8s_token" value="{{.k8s_token}}">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<!-- Email -->
|
||||
<details class="optional field">
|
||||
@@ -304,6 +303,18 @@
|
||||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_devcontainer_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_devcontainer"}}</label>
|
||||
<input name="default_allow_create_devcontainer" type="checkbox" {{if .default_allow_create_devcontainer}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_actrunner_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_actrunner"}}</label>
|
||||
<input name="default_allow_create_actrunner" type="checkbox" {{if .default_allow_create_actrunner}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
|
||||
|
||||
449
templates/repo/actions/debug.tmpl
Normal file
449
templates/repo/actions/debug.tmpl
Normal file
@@ -0,0 +1,449 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository actions debug-page">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
<div class="ui stackable grid">
|
||||
<!-- Left Panel: Workflow Editor -->
|
||||
<div class="eight wide column">
|
||||
<div class="ui segment">
|
||||
<h3 class="ui header">
|
||||
{{ctx.Locale.Tr "actions.workflow.debug"}}
|
||||
<span class="ui grey text">({{.WorkflowID}})</span>
|
||||
</h3>
|
||||
|
||||
<div class="ui form">
|
||||
<!-- Workflow Editor -->
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.content"}}</label>
|
||||
<textarea id="workflowEditor" class="ui textarea" style="height: 400px; font-family: monospace;" placeholder="Workflow YAML content"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Debug Parameters -->
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.ref"}}</label>
|
||||
<select id="refSelect" class="ui dropdown">
|
||||
<option value="">{{ctx.Locale.Tr "repo.default_branch"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.event"}}</label>
|
||||
<select id="eventSelect" class="ui dropdown">
|
||||
<option value="push">push</option>
|
||||
<option value="pull_request">pull_request</option>
|
||||
<option value="workflow_dispatch">workflow_dispatch</option>
|
||||
<option value="issues">issues</option>
|
||||
<option value="issue_comment">issue_comment</option>
|
||||
<option value="pull_request_review">pull_request_review</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflow Inputs -->
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.inputs"}}</label>
|
||||
<div id="inputsContainer" class="ui segment"></div>
|
||||
<button type="button" class="ui mini button" id="addInputBtn">
|
||||
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.env"}}</label>
|
||||
<div id="envContainer" class="ui segment"></div>
|
||||
<button type="button" class="ui mini button" id="addEnvBtn">
|
||||
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="ui form-actions">
|
||||
<button type="button" class="ui primary button" id="runBtn" onclick="runDebugWorkflow()">
|
||||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.workflow.run"}}
|
||||
</button>
|
||||
<a href="{{$.ActionsURL}}" class="ui button">{{ctx.Locale.Tr "cancel"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Logs and Status -->
|
||||
<div class="eight wide column">
|
||||
<div class="ui segment">
|
||||
<h3 class="ui header">
|
||||
{{ctx.Locale.Tr "actions.workflow.logs"}}
|
||||
</h3>
|
||||
|
||||
<!-- Status Info -->
|
||||
<div id="statusInfo" style="display: none;" class="ui info message">
|
||||
<p>{{ctx.Locale.Tr "actions.workflow.running"}}: <a id="runLink" href="#" target="_blank"></a></p>
|
||||
</div>
|
||||
|
||||
<!-- Logs Container -->
|
||||
<div id="logsContainer" style="border: 1px solid #ddd; padding: 10px; height: 500px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace; font-size: 12px;">
|
||||
<p style="color: #999;">{{ctx.Locale.Tr "actions.workflow.logs_placeholder"}}</p>
|
||||
</div>
|
||||
|
||||
<!-- Refresh and Copy Buttons -->
|
||||
<div style="margin-top: 10px;">
|
||||
<button type="button" class="ui mini button" id="refreshBtn" onclick="refreshLogs()" style="display: none;">
|
||||
{{svg "octicon-sync"}} {{ctx.Locale.Tr "refresh"}}
|
||||
</button>
|
||||
<button type="button" class="ui mini button" id="copyBtn" onclick="copyLogs()" style="display: none;">
|
||||
{{svg "octicon-copy"}} {{ctx.Locale.Tr "copy"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
<div id="errorMessage" style="display: none;" class="ui error message">
|
||||
<p id="errorText"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const debugSessionID = {{.DebugSessionID}};
|
||||
const workflowID = "{{.WorkflowID}}";
|
||||
const defaultBranch = "{{.DefaultBranch}}";
|
||||
const actionsURL = "{{.ActionsURL}}";
|
||||
const debugAPIURL = "{{.DebugAPIURL}}";
|
||||
const csrfToken = "{{.CsrfToken}}";
|
||||
|
||||
let currentRunIndex = null;
|
||||
let logsAutoRefreshInterval = null;
|
||||
|
||||
// Initialize editor with workflow content
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const editor = document.getElementById('workflowEditor');
|
||||
editor.value = `{{.WorkflowContent}}`;
|
||||
|
||||
// Initialize dropdowns
|
||||
$('.ui.dropdown').dropdown();
|
||||
|
||||
// Populate ref select with branches and tags
|
||||
populateRefs();
|
||||
|
||||
// Setup input/env add buttons
|
||||
document.getElementById('addInputBtn').addEventListener('click', addInputField);
|
||||
document.getElementById('addEnvBtn').addEventListener('click', addEnvField);
|
||||
});
|
||||
|
||||
function populateRefs() {
|
||||
const refSelect = document.getElementById('refSelect');
|
||||
// Add default branch
|
||||
const option = document.createElement('option');
|
||||
option.value = defaultBranch;
|
||||
option.text = defaultBranch + ' (default)';
|
||||
refSelect.appendChild(option);
|
||||
// TODO: fetch branches and tags from API
|
||||
}
|
||||
|
||||
function addInputField() {
|
||||
const container = document.getElementById('inputsContainer');
|
||||
const id = 'input_' + Date.now();
|
||||
const field = document.createElement('div');
|
||||
field.className = 'ui form';
|
||||
field.id = id;
|
||||
field.innerHTML = `
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_name"}}" class="input-key">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_value"}}" class="input-value">
|
||||
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
|
||||
{{svg "octicon-trash"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(field);
|
||||
}
|
||||
|
||||
function addEnvField() {
|
||||
const container = document.getElementById('envContainer');
|
||||
const id = 'env_' + Date.now();
|
||||
const field = document.createElement('div');
|
||||
field.className = 'ui form';
|
||||
field.id = id;
|
||||
field.innerHTML = `
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_name"}}" class="env-key">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_value"}}" class="env-value">
|
||||
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
|
||||
{{svg "octicon-trash"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(field);
|
||||
}
|
||||
|
||||
function getInputsObject() {
|
||||
const inputs = {};
|
||||
document.querySelectorAll('#inputsContainer .input-key').forEach((key, index) => {
|
||||
const keyElem = document.querySelectorAll('#inputsContainer .input-key')[index];
|
||||
const valueElem = document.querySelectorAll('#inputsContainer .input-value')[index];
|
||||
if (keyElem.value && valueElem.value) {
|
||||
inputs[keyElem.value] = valueElem.value;
|
||||
}
|
||||
});
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function getEnvObject() {
|
||||
const env = {};
|
||||
document.querySelectorAll('#envContainer .env-key').forEach((key, index) => {
|
||||
const keyElem = document.querySelectorAll('#envContainer .env-key')[index];
|
||||
const valueElem = document.querySelectorAll('#envContainer .env-value')[index];
|
||||
if (keyElem.value && valueElem.value) {
|
||||
env[keyElem.value] = valueElem.value;
|
||||
}
|
||||
});
|
||||
return env;
|
||||
}
|
||||
|
||||
function runDebugWorkflow() {
|
||||
const workflowContent = document.getElementById('workflowEditor').value;
|
||||
const ref = document.getElementById('refSelect').value || defaultBranch;
|
||||
const event = document.getElementById('eventSelect').value;
|
||||
const inputs = getInputsObject();
|
||||
const env = getEnvObject();
|
||||
|
||||
if (!workflowContent) {
|
||||
showError('{{ctx.Locale.Tr "actions.workflow.content_empty"}}');
|
||||
return;
|
||||
}
|
||||
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
runBtn.classList.add('loading');
|
||||
runBtn.disabled = true;
|
||||
|
||||
const payload = {
|
||||
workflow_content: workflowContent,
|
||||
ref: ref,
|
||||
event: event,
|
||||
inputs: inputs,
|
||||
env: env
|
||||
};
|
||||
|
||||
console.log('Submitting to:', `${debugAPIURL}/${debugSessionID}/run`);
|
||||
console.log('Payload:', payload);
|
||||
console.log('CSRF Token:', csrfToken);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Csrf-Token': csrfToken,
|
||||
};
|
||||
|
||||
fetch(`${debugAPIURL}/${debugSessionID}/run`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Response status:', response.status, 'OK:', response.ok);
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
console.log('Error response:', text);
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Response data:', data);
|
||||
if (data.success || data.run_index) {
|
||||
currentRunIndex = data.run_index;
|
||||
document.getElementById('statusInfo').style.display = 'block';
|
||||
document.getElementById('runLink').href = `${actionsURL}/runs/${currentRunIndex}`;
|
||||
document.getElementById('runLink').textContent = `Run #${currentRunIndex}`;
|
||||
document.getElementById('errorMessage').style.display = 'none';
|
||||
document.getElementById('refreshBtn').style.display = 'inline-block';
|
||||
document.getElementById('copyBtn').style.display = 'inline-block';
|
||||
|
||||
// Start auto-refreshing logs
|
||||
refreshLogs();
|
||||
logsAutoRefreshInterval = setInterval(refreshLogs, 2000);
|
||||
} else {
|
||||
showError(data.error || data.message || '{{ctx.Locale.Tr "error"}}');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
showError('Failed to run workflow: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
runBtn.classList.remove('loading');
|
||||
runBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshLogs() {
|
||||
if (!currentRunIndex) return;
|
||||
|
||||
console.log('Refreshing logs for run index:', currentRunIndex);
|
||||
|
||||
// Get logs from the action run API via POST
|
||||
const payload = {
|
||||
LogCursors: []
|
||||
};
|
||||
|
||||
fetch(`${actionsURL}/runs/${currentRunIndex}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Csrf-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Log fetch response status:', response.status, response.statusText);
|
||||
console.log('Content-Type:', response.headers.get('Content-Type'));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Check if response is JSON
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json();
|
||||
} else {
|
||||
// If not JSON, get text and try to parse
|
||||
return response.text().then(text => {
|
||||
console.log('Response is not JSON, attempting to parse');
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response as JSON:', text.substring(0, 200));
|
||||
throw new Error('Response is not valid JSON. Content: ' + text.substring(0, 500));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Log data received:', data);
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
|
||||
if (!data) {
|
||||
logsContainer.textContent = 'No response data';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.logs && data.logs.stepsLog && data.logs.stepsLog.length > 0) {
|
||||
let logContent = '';
|
||||
data.logs.stepsLog.forEach((stepLog, index) => {
|
||||
console.log(`Step ${index}:`, stepLog);
|
||||
|
||||
if (stepLog.rawOutput) {
|
||||
logContent += stepLog.rawOutput + '\n';
|
||||
} else if (stepLog.lines && Array.isArray(stepLog.lines)) {
|
||||
stepLog.lines.forEach(line => {
|
||||
if (line && line.content) {
|
||||
logContent += line.content + '\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (logContent.trim()) {
|
||||
logsContainer.textContent = logContent;
|
||||
} else {
|
||||
logsContainer.textContent = 'Waiting for logs...';
|
||||
}
|
||||
} else if (data.state && data.state.run) {
|
||||
// Show run status if logs not ready
|
||||
logsContainer.textContent = 'Status: ' + (data.state.run.status || 'waiting') + '\n\nWaiting for logs...';
|
||||
} else {
|
||||
logsContainer.textContent = 'Waiting for logs...';
|
||||
}
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching logs:', error);
|
||||
document.getElementById('logsContainer').textContent = 'Error: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
function copyLogs() {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
const text = logsContainer.textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Show success message
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
const originalText = copyBtn.innerHTML;
|
||||
copyBtn.innerHTML = '{{svg "octicon-check"}} Copied!';
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorMsg = document.getElementById('errorMessage');
|
||||
document.getElementById('errorText').textContent = message;
|
||||
errorMsg.style.display = 'block';
|
||||
// Clear after 5 seconds
|
||||
setTimeout(() => {
|
||||
errorMsg.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (logsAutoRefreshInterval) {
|
||||
clearInterval(logsAutoRefreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.debug-page .ui.segment {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.debug-page #workflowEditor {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.debug-page #logsContainer {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.debug-page .ui.form-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid rgba(34, 36, 38, 0.15);
|
||||
}
|
||||
|
||||
.debug-page .ui.form-actions .button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.debug-page .input-key,
|
||||
.debug-page .input-value,
|
||||
.debug-page .env-key,
|
||||
.debug-page .env-value {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{template "base/footer" .}}
|
||||
@@ -65,6 +65,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .CurWorkflow}}
|
||||
<a href="{{$.Link}}/debug?workflow={{$.CurWorkflow}}" class="ui button">
|
||||
{{svg "octicon-bug"}} {{ctx.Locale.Tr "actions.workflow.debug"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .AllowDisableOrEnableWorkflow}}
|
||||
<button class="ui jump dropdown btn interact-bg tw-p-2">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"echo \"postCreateCommand\"",
|
||||
"echo \"OK\""
|
||||
],
|
||||
"postAttachCommand": [
|
||||
"echo \"postAttachCommand\"",
|
||||
"echo \"OK\""
|
||||
],
|
||||
"runArgs": [
|
||||
"-p 8888"
|
||||
]
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
{{else}}
|
||||
|
||||
<div class="ui container">
|
||||
|
||||
<form class="ui edit form">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field">
|
||||
@@ -36,7 +37,9 @@
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
|
||||
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -47,7 +50,7 @@
|
||||
<strong>{{ctx.Locale.Tr "repo.dev_container_control"}}</strong>
|
||||
<div class="ui relaxed list">
|
||||
|
||||
{{if .HasDevContainer}}
|
||||
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
|
||||
<div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/devcontainer/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
|
||||
{{if .isAdmin}}
|
||||
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
|
||||
@@ -66,7 +69,7 @@
|
||||
<div style=" display: none;" id="createContainer" class="item">
|
||||
<div>
|
||||
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
|
||||
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</button>
|
||||
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_control.create"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,6 +87,16 @@
|
||||
<!-- 结束Dev Container 正文内容 -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 自定义警告框 -->
|
||||
<div id="customAlert" class="custom-alert">
|
||||
<div class="alert-content">
|
||||
<div class="alert-header">
|
||||
<strong>提示信息</strong>
|
||||
<button class="alert-close" onclick="closeCustomAlert()">×</button>
|
||||
</div>
|
||||
<div id="alertText" class="alert-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认删除 Dev Container 模态对话框 -->
|
||||
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
||||
@@ -96,24 +109,14 @@
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
<!-- 确认 Dev Container 模态对话框 -->
|
||||
<!-- 保存 Dev Container 模态对话框 -->
|
||||
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
{{if not .HasDevContainerDockerfile}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
|
||||
{{else}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
|
||||
{{end}}
|
||||
<label for="SaveMethod">Build From Dockerfile</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="required field ">
|
||||
<label for="RepositoryAddress">Registry:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
|
||||
@@ -124,13 +127,38 @@
|
||||
</div>
|
||||
<div class="required field ">
|
||||
<label for="RepositoryPassword">Registry Password:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="RepositoryPassword" name="RepositoryPassword" required>
|
||||
<div style="position: relative; display: inline-block; width: 100%;">
|
||||
<input style="border: 1px solid black; width: 100%; padding-right: 80px;"
|
||||
type="password"
|
||||
id="RepositoryPassword"
|
||||
name="RepositoryPassword"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
<button type="button"
|
||||
style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer; color: #666;
|
||||
font-size: 12px; padding: 5px 8px;"
|
||||
onclick="togglePasswordVisibility('RepositoryPassword', this)">
|
||||
显示密码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="required field ">
|
||||
<label for="ImageName">Image(name:tag):</label>
|
||||
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
{{if not .HasDevContainerDockerfile}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
|
||||
<label for="SaveMethod">There is no Dockerfile</label>
|
||||
{{else}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
|
||||
<label for="SaveMethod">Build From Dockerfile: {{.DockerfilePath}}</label>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
|
||||
<button class="ui cancel button" id="updateCloseButton">Close</button>
|
||||
@@ -143,6 +171,21 @@
|
||||
|
||||
|
||||
<script>
|
||||
document.getElementById('updateSubmitButton').addEventListener('click', function() {
|
||||
const form = document.getElementById('updateForm');
|
||||
const formData = new FormData(form);
|
||||
var RepositoryAddress = formData.get('RepositoryAddress');
|
||||
var RepositoryUsername = formData.get('RepositoryUsername');
|
||||
var RepositoryPassword = formData.get('RepositoryPassword');
|
||||
var SaveMethod = formData.get('SaveMethod');
|
||||
var ImageName = formData.get('ImageName');
|
||||
if(ImageName != "" && SaveMethod != "" && RepositoryPassword != "" && RepositoryUsername != "" && RepositoryAddress != ""){
|
||||
document.getElementById('updatemodal').classList.add('is-loading')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
var status = '-1'
|
||||
var intervalID
|
||||
const createContainer = document.getElementById('createContainer');
|
||||
@@ -233,13 +276,13 @@ function getStatus() {
|
||||
if(status !== '9' && status !== '-1' && data.status == '9'){
|
||||
window.location.reload();
|
||||
}
|
||||
if(status !== '-1' && data.status == '-1'){
|
||||
else if(status !== '-1' && data.status == '-1'){
|
||||
window.location.reload();
|
||||
}
|
||||
if(status !== '4' && status !== '-1' && data.status == '4'){
|
||||
window.location.reload();
|
||||
else if(status !== '4' && status !== '-1' && data.status == '4'){
|
||||
//window.location.reload();
|
||||
}
|
||||
if (data.status == '-1' || data.status == '') {
|
||||
else if (data.status == '-1' || data.status == '') {
|
||||
if (loadingElement) {
|
||||
loadingElement.style.display = 'none';
|
||||
}
|
||||
@@ -333,7 +376,7 @@ function getStatus() {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
intervalID = setInterval(getStatus, 3000);
|
||||
intervalID = setInterval(getStatus, 5000);
|
||||
if (restartContainer) {
|
||||
restartContainer.addEventListener('click', function(event) {
|
||||
// 处理点击逻辑
|
||||
@@ -342,7 +385,7 @@ if (restartContainer) {
|
||||
loadingElement.style.display = 'block';
|
||||
}
|
||||
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
|
||||
.then(response => {intervalID = setInterval(getStatus, 3000);})
|
||||
.then(response => {intervalID = setInterval(getStatus, 5000);})
|
||||
});
|
||||
}
|
||||
if (stopContainer) {
|
||||
@@ -353,7 +396,7 @@ if (stopContainer) {
|
||||
}
|
||||
// 处理点击逻辑
|
||||
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
|
||||
.then(response => {intervalID = setInterval(getStatus, 3000);})
|
||||
.then(response => {intervalID = setInterval(getStatus, 5000);})
|
||||
|
||||
});
|
||||
}
|
||||
@@ -363,10 +406,46 @@ if (deleteContainer) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function togglePasswordVisibility(passwordFieldId, button) {
|
||||
const passwordInput = document.getElementById(passwordFieldId);
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
button.textContent = '隐藏密码';
|
||||
button.style.color = '#2185d0'; // 主色调,表示激活状态
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
button.textContent = '显示密码';
|
||||
button.style.color = '#666'; // 恢复默认颜色
|
||||
}
|
||||
}
|
||||
function showCustomAlert(message, title = "提示信息") {
|
||||
const alertBox = document.getElementById('customAlert');
|
||||
const alertText = document.getElementById('alertText');
|
||||
const alertHeader = alertBox.querySelector('.alert-header strong');
|
||||
|
||||
alertHeader.textContent = title;
|
||||
alertText.textContent = message;
|
||||
alertBox.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeCustomAlert() {
|
||||
document.getElementById('customAlert').style.display = 'none';
|
||||
}
|
||||
|
||||
// 点击背景关闭
|
||||
document.getElementById('customAlert').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeCustomAlert();
|
||||
}
|
||||
});
|
||||
|
||||
function submitForm(event) {
|
||||
event.preventDefault(); // 阻止默认的表单提交行为
|
||||
const {csrfToken} = window.config;
|
||||
const {appSubUrl} = window.config;
|
||||
const formModal = document.getElementById('updatemodal');
|
||||
const form = document.getElementById('updateForm');
|
||||
const submitButton = document.getElementById('updateSubmitButton');
|
||||
const closeButton = document.getElementById('updateCloseButton');
|
||||
@@ -390,9 +469,10 @@ function submitForm(event) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
submitButton.disabled = false;
|
||||
alert(data.message);
|
||||
formModal.classList.remove('is-loading')
|
||||
showCustomAlert(data.message);
|
||||
if(data.redirect){
|
||||
closeButton.click()
|
||||
closeCustomAlert()
|
||||
}
|
||||
intervalID = setInterval(getStatus, 3000);
|
||||
})
|
||||
@@ -422,6 +502,69 @@ function submitForm(event) {
|
||||
0%{-webkit-transform:rotate(0deg)}
|
||||
100%{-webkit-transform:rotate(360deg)}
|
||||
}
|
||||
|
||||
.custom-alert {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 10000;
|
||||
}
|
||||
.alert-content {
|
||||
color: black;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 0; /* 移除内边距,在内部元素中设置 */
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
max-height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
.alert-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.alert-close {
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.alert-close:hover {
|
||||
background: #e9ecef;
|
||||
color: #000;
|
||||
}
|
||||
.alert-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(80vh - 100px); /* 减去头部高度 */
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
{{template "base/footer" .}}
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</button>
|
||||
<div class="menu">
|
||||
{{if or (.AllowCreateActRunner)}}
|
||||
<div class="item">
|
||||
<a href="{{$.Link}}/regist_runner">{{ctx.Locale.Tr "actions.runners.regist_runner"}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="item">
|
||||
<a href="https://docs.gitea.com/usage/actions/act-runner">{{ctx.Locale.Tr "actions.runners.new_notice"}}</a>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ test.beforeAll(async ({browser}, workerInfo) => {
|
||||
test('homepage', async ({page}) => {
|
||||
const response = await page.goto('/');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/);
|
||||
await expect(page).toHaveTitle(/^DevStar: The Last Mile of Al for R&D\s*$/);
|
||||
await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
APP_NAME = DevStar: The Last Mile of Al for R&D
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
APP_NAME = DevStar: The Last Mile of Al for R&D
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
APP_NAME = DevStar: The Last Mile of Al for R&D
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
APP_NAME = DevStar: The Last Mile of Al for R&D
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
|
||||
Reference in New Issue
Block a user