Skip to content

Commit 21f0522

Browse files
feat: 支持在 user 下直接创建项目
- 在 UserSpec 和 UserOutput 中添加 Projects 字段 - 实现 createUserProjectsWithOutput 函数,支持用户级项目创建 - 实现 deleteUserProjects 函数,支持用户级项目清理 - 用户级项目使用用户的命名空间(username/project-path) - 更新示例配置文件和文档
1 parent 694b600 commit 21f0522

File tree

4 files changed

+160
-14
lines changed

4 files changed

+160
-14
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ users:
149149
path: test-go-api
150150
description: Go API 服务
151151
visibility: private
152+
153+
# 用户级项目(不属于任何组,直接在用户命名空间下)
154+
projects:
155+
- name: my-personal-project
156+
path: my-personal-project
157+
description: Personal project under user namespace
158+
visibility: private
152159
```
153160
154161
### Token 配置说明
@@ -250,6 +257,13 @@ users:
250257
description: Go API 服务
251258
visibility: private
252259
web_url: https://devops-gitlab.alaudatech.net/tektoncd-backend-group/test-go-api
260+
projects:
261+
- name: my-personal-project
262+
path: tektoncd/my-personal-project
263+
project_id: 1438
264+
description: Personal project under user namespace
265+
visibility: private
266+
web_url: https://devops-gitlab.alaudatech.net/tektoncd/my-personal-project
253267
```
254268

255269
### 自定义模板输出

examples/user.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ users:
3131
description: demo project
3232
visibility: private
3333

34+
# 用户级项目(不属于任何组,直接在用户命名空间下)
35+
projects:
36+
- name: my-personal-project
37+
path: my-personal-project
38+
description: Personal project under user namespace
39+
visibility: private
40+
3441
# 示例 2: name 模式(不添加时间戳)
3542
# 使用方法:
3643
# 1. 创建:./bin/gitlab-cli user create -f user.yaml

internal/processor/processor.go

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ func (p *ResourceProcessor) ProcessUserCreation(userSpec types.UserSpec) (*types
8686
output.Groups = groupOutputs
8787
}
8888

89+
// 4. 创建用户级项目(不属于任何组的项目)
90+
if len(userSpec.Projects) > 0 {
91+
log.Printf(" 创建 %d 个用户级项目...\n", len(userSpec.Projects))
92+
projectOutputs, err := p.createUserProjectsWithOutput(actualUsername, userSpec.Projects, nameMode)
93+
if err != nil {
94+
log.Printf(" ⚠ 创建用户级项目失败: %v\n", err)
95+
} else {
96+
output.Projects = projectOutputs
97+
}
98+
}
99+
89100
return output, nil
90101
}
91102

@@ -224,6 +235,85 @@ func (p *ResourceProcessor) ensureGroup(username string, groupSpec types.GroupSp
224235
return group.ID, group.Path, nil
225236
}
226237

238+
// createUserProjectsWithOutput 创建用户级别的项目(不属于任何组)
239+
func (p *ResourceProcessor) createUserProjectsWithOutput(username string, projects []types.ProjectSpec, userNameMode string) ([]types.ProjectOutput, error) {
240+
var projectOutputs []types.ProjectOutput
241+
242+
// 获取用户信息以获取用户的 namespace ID
243+
user, err := p.Client.GetUser(username)
244+
if err != nil || user == nil {
245+
return nil, fmt.Errorf("获取用户信息失败: %w", err)
246+
}
247+
248+
for _, projSpec := range projects {
249+
// 确定项目的 nameMode(如果项目没有指定,则继承用户的 nameMode)
250+
projectNameMode := projSpec.NameMode
251+
if projectNameMode == "" {
252+
projectNameMode = userNameMode
253+
}
254+
255+
// 根据 nameMode 生成实际的 project path
256+
var actualProjectPath string
257+
if projectNameMode == "name" {
258+
// name 模式:直接使用配置文件中的名称
259+
actualProjectPath = projSpec.Path
260+
if actualProjectPath == "" {
261+
actualProjectPath = projSpec.Name
262+
}
263+
log.Printf(" 使用 name 模式,项目 path: %s\n", actualProjectPath)
264+
} else {
265+
// prefix 模式:添加时间戳
266+
if projSpec.Path == "" {
267+
actualProjectPath = utils.GenerateProjectPathWithTimestamp(projSpec.Name)
268+
} else {
269+
actualProjectPath = utils.GenerateProjectPathWithTimestamp(projSpec.Path)
270+
}
271+
log.Printf(" 使用 prefix 模式,生成项目 path: %s\n", actualProjectPath)
272+
}
273+
274+
// 用户级项目的 full path 是 username/project-path
275+
fullPath := fmt.Sprintf("%s/%s", username, actualProjectPath)
276+
existingProj, _ := p.Client.GetProject(fullPath)
277+
278+
var projectID int
279+
var webURL string
280+
281+
if existingProj != nil {
282+
log.Printf(" ⚠ 项目 '%s' 已存在 (ID: %d)\n", projSpec.Name, existingProj.ID)
283+
projectID = existingProj.ID
284+
webURL = existingProj.WebURL
285+
} else {
286+
log.Printf(" 创建用户级项目: %s (path: %s)\n", projSpec.Name, actualProjectPath)
287+
// 用户级项目使用用户的 ID 作为 namespace ID
288+
project, err := p.Client.CreateProject(
289+
username,
290+
user.ID, // 使用用户 ID 作为 namespace ID
291+
projSpec.Name,
292+
actualProjectPath,
293+
projSpec.Description,
294+
utils.GetVisibility(projSpec.Visibility),
295+
)
296+
if err != nil {
297+
log.Printf(" ⚠ 创建项目失败 %s: %v\n", projSpec.Name, err)
298+
continue
299+
}
300+
log.Printf(" ✓ 项目创建成功 (ID: %d, Path: %s)\n", project.ID, project.PathWithNamespace)
301+
projectID = project.ID
302+
webURL = project.WebURL
303+
}
304+
305+
projectOutputs = append(projectOutputs, types.ProjectOutput{
306+
Name: projSpec.Name,
307+
Path: fullPath,
308+
ProjectID: projectID,
309+
Description: projSpec.Description,
310+
Visibility: projSpec.Visibility,
311+
WebURL: webURL,
312+
})
313+
}
314+
return projectOutputs, nil
315+
}
316+
227317
// createProjectsWithOutput 创建多个项目并返回输出结果
228318
func (p *ResourceProcessor) createProjectsWithOutput(username string, groupID int, groupPath string, projects []types.ProjectSpec, groupNameMode string) ([]types.ProjectOutput, error) {
229319
var projectOutputs []types.ProjectOutput
@@ -314,7 +404,13 @@ func (p *ResourceProcessor) ProcessUserCleanup(userSpec types.UserSpec) error {
314404

315405
log.Printf(" 找到用户 '%s' (ID: %d, 邮箱: %s)\n", user.Username, user.ID, user.Email)
316406

317-
// 1. 删除配置文件中定义的组和项目
407+
// 1. 删除用户级项目(不属于任何组的项目)
408+
if len(userSpec.Projects) > 0 {
409+
log.Printf(" 删除 %d 个用户级项目...\n", len(userSpec.Projects))
410+
p.deleteUserProjects(userSpec.Username, userSpec.Projects)
411+
}
412+
413+
// 2. 删除配置文件中定义的组和项目
318414
if len(userSpec.Groups) > 0 {
319415
log.Printf(" 删除 %d 个组及其项目...\n", len(userSpec.Groups))
320416
p.deleteConfiguredGroups(userSpec.Groups)
@@ -341,6 +437,33 @@ func (p *ResourceProcessor) ProcessUserCleanup(userSpec types.UserSpec) error {
341437
return nil
342438
}
343439

440+
// deleteUserProjects 删除用户级项目
441+
func (p *ResourceProcessor) deleteUserProjects(username string, projects []types.ProjectSpec) {
442+
for i, projSpec := range projects {
443+
log.Printf(" ------------------------------------------\n")
444+
log.Printf(" 处理用户级项目 [%d/%d]: %s\n", i+1, len(projects), projSpec.Name)
445+
446+
// 用户级项目的 full path 是 username/project-path
447+
projectPath := projSpec.Path
448+
if projectPath == "" {
449+
projectPath = projSpec.Name
450+
}
451+
fullPath := fmt.Sprintf("%s/%s", username, projectPath)
452+
project, _ := p.Client.GetProject(fullPath)
453+
454+
if project != nil {
455+
log.Printf(" 删除项目: %s (ID: %d)\n", projSpec.Name, project.ID)
456+
if err := p.Client.DeleteProject(project.ID); err != nil {
457+
log.Printf(" ⚠ 删除项目失败: %v\n", err)
458+
} else {
459+
log.Printf(" ✓ 项目删除成功\n")
460+
}
461+
} else {
462+
log.Printf(" ⚠ 项目不存在,跳过: %s\n", fullPath)
463+
}
464+
}
465+
}
466+
344467
// deleteConfiguredGroups 删除配置文件中定义的组及其项目
345468
func (p *ResourceProcessor) deleteConfiguredGroups(groups []types.GroupSpec) {
346469
for j, groupSpec := range groups {

pkg/types/types.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ type UserConfig struct {
77

88
// UserSpec 用户规格定义
99
type UserSpec struct {
10-
NameMode string `yaml:"nameMode,omitempty"` // 命名模式: "prefix" (添加时间戳) 或 "name" (不添加时间戳),默认为 "prefix"
11-
Username string `yaml:"username"`
12-
Email string `yaml:"email"`
13-
Name string `yaml:"name"`
14-
Password string `yaml:"password"`
15-
Token *TokenSpec `yaml:"token"` // Personal Access Token 配置
16-
Groups []GroupSpec `yaml:"groups"` // 支持多个组
10+
NameMode string `yaml:"nameMode,omitempty"` // 命名模式: "prefix" (添加时间戳) 或 "name" (不添加时间戳),默认为 "prefix"
11+
Username string `yaml:"username"`
12+
Email string `yaml:"email"`
13+
Name string `yaml:"name"`
14+
Password string `yaml:"password"`
15+
Token *TokenSpec `yaml:"token"` // Personal Access Token 配置
16+
Groups []GroupSpec `yaml:"groups"` // 支持多个组
17+
Projects []ProjectSpec `yaml:"projects"` // 用户级别的项目(不属于任何组)
1718
}
1819

1920
// TokenSpec Personal Access Token 规格定义
@@ -54,12 +55,13 @@ type OutputConfig struct {
5455

5556
// UserOutput 用户输出结果
5657
type UserOutput struct {
57-
Username string `yaml:"username"`
58-
Email string `yaml:"email"`
59-
Name string `yaml:"name"`
60-
UserID int `yaml:"user_id"`
61-
Token *TokenOutput `yaml:"token,omitempty"`
62-
Groups []GroupOutput `yaml:"groups,omitempty"`
58+
Username string `yaml:"username"`
59+
Email string `yaml:"email"`
60+
Name string `yaml:"name"`
61+
UserID int `yaml:"user_id"`
62+
Token *TokenOutput `yaml:"token,omitempty"`
63+
Groups []GroupOutput `yaml:"groups,omitempty"`
64+
Projects []ProjectOutput `yaml:"projects,omitempty"` // 用户级别的项目
6365
}
6466

6567
// TokenOutput Token 输出结果

0 commit comments

Comments
 (0)