Skip to content

Commit e38d47c

Browse files
committed
Add support for project namespaces configuration
1 parent 44fff86 commit e38d47c

File tree

8 files changed

+259
-22
lines changed

8 files changed

+259
-22
lines changed

cmd/run.go

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,6 @@ func (c *RunConfig) Exec(ctx context.Context, _ []string) error {
110110
cfg.Log.Level = "debug"
111111
}
112112

113-
// add projects passed to run command
114-
for _, pid := range c.projects {
115-
exists := slices.ContainsFunc(cfg.Projects, func(p config.Project) bool {
116-
return p.Id == pid
117-
})
118-
119-
if !exists {
120-
cfg.Projects = append(cfg.Projects, config.Project{
121-
ProjectSettings: config.DefaultProjectSettings(),
122-
Id: pid,
123-
})
124-
}
125-
}
126-
127113
if cfg.Log.Level == "debug" {
128114
writeConfig(c.out, cfg)
129115
}
@@ -147,6 +133,26 @@ func (c *RunConfig) Exec(ctx context.Context, _ []string) error {
147133
return err
148134
}
149135

136+
// gather projects from config
137+
projects, err := resolveProjects(ctx, cfg, gitlabclient)
138+
if err != nil {
139+
return fmt.Errorf("error resolving projects: %w", err)
140+
}
141+
142+
// add projects passed as arguments
143+
for _, pid := range c.projects {
144+
exists := slices.ContainsFunc(cfg.Projects, func(p config.Project) bool {
145+
return p.Id == pid
146+
})
147+
148+
if !exists {
149+
projects = append(cfg.Projects, config.Project{
150+
ProjectSettings: config.DefaultProjectSettings(),
151+
Id: pid,
152+
})
153+
}
154+
}
155+
150156
g := &run.Group{}
151157

152158
pool := worker.NewWorkerPool(42)
@@ -166,12 +172,12 @@ func (c *RunConfig) Exec(ctx context.Context, _ []string) error {
166172
})
167173
}
168174

169-
if len(cfg.Projects) > 0 { // jobs
175+
if len(projects) > 0 { // jobs
170176
ctx, cancel := context.WithCancel(context.Background())
171177

172178
g.Add(func() error { // execute
173179
var wg sync.WaitGroup
174-
for _, p := range cfg.Projects {
180+
for _, p := range projects {
175181
if c.catchup && p.CatchUp.Enabled {
176182
job := jobs.ProjectCatchUpJob{
177183
Config: p,
@@ -288,3 +294,39 @@ func serveHTTP(cfg config.HTTP, reg *prometheus.Registry) (func() error, func(er
288294

289295
return execute, interrupt
290296
}
297+
298+
func resolveProjects(ctx context.Context, cfg config.Config, glab *gitlab.Client) ([]config.Project, error) {
299+
pm := make(map[int64]config.Project)
300+
301+
opt := gitlab.ListNamespaceProjectsOptions{}
302+
for _, namespace := range cfg.Namespaces {
303+
opt.Kind = namespace.Kind
304+
opt.Visibility = (*gitlab.VisibilityValue)(&namespace.Visibility)
305+
opt.WithShared = namespace.WithShared
306+
opt.IncludeSubgroups = namespace.IncludeSubgroups
307+
308+
ps, err := glab.ListNamespaceProjects(ctx, namespace.Id, opt)
309+
if err != nil {
310+
return nil, err
311+
}
312+
313+
for _, p := range ps {
314+
pm[p.Id] = config.Project{
315+
ProjectSettings: namespace.ProjectSettings,
316+
Id: p.Id,
317+
}
318+
}
319+
}
320+
321+
// overwrite with explicitly configured projects
322+
for _, p := range cfg.Projects {
323+
pm[p.Id] = p
324+
}
325+
326+
projects := make([]config.Project, 0, len(pm))
327+
for _, p := range pm {
328+
projects = append(projects, p)
329+
}
330+
331+
return projects, nil
332+
}

configs/gitlab-exporter.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@ projects: []
6161
# export: {}
6262
# catch_up: {}
6363

64+
# List of namespaces of which to export projects
65+
namespaces: []
66+
# - # The namespace id or url-encoded path
67+
# id: gitlab-exporter
68+
#
69+
# # The namespace kind (user or group).
70+
# # Optional, will be determined if not specified.
71+
# kind: group
72+
#
73+
# # Limit by visibility (public, internal or private)
74+
# visibility: ""
75+
#
76+
# # Include projects shared to this group.
77+
# # (Only applicable for group namespaces)
78+
# with_shared: false
79+
#
80+
# # Whether to include projects in subgroups of this namespace.
81+
# # (Only applicable for group namespaces)
82+
# include_subgroups: false
83+
#
84+
# # See `project_defaults` for settings that can be overwritten here.
85+
# export: {}
86+
# catch_up: {}
87+
6488
# HTTP server settings
6589
http:
6690
# Whether to enable serving http endpoint (metrics, debug info)

internal/config/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type Config struct {
1414
ProjectDefaults ProjectSettings `default:"{}" yaml:"project_defaults"`
1515
// List of project to export
1616
Projects []Project `default:"[]" yaml:"projects"`
17+
// List of namespaces of which to export projects
18+
Namespaces []Namespace `default:"[]" yaml:"namespaces"`
1719
// HTTP server settings
1820
HTTP HTTP `default:"{}" yaml:"http"`
1921
// Log configuration settings
@@ -77,6 +79,17 @@ type ProjectCatchUp struct {
7779
UpdatedBefore string `default:"" yaml:"updated_before"`
7880
}
7981

82+
type Namespace struct {
83+
ProjectSettings `default:"{}" yaml:",inline"`
84+
85+
Id string `yaml:"id"`
86+
Kind string `default:"" yaml:"kind"`
87+
88+
Visibility string `default:"" yaml:"visibility"`
89+
WithShared bool `default:"false" yaml:"with_shared"`
90+
IncludeSubgroups bool `default:"false" yaml:"include_subgroups"`
91+
}
92+
8093
type HTTP struct {
8194
Enabled bool `default:"true" yaml:"enabled"`
8295
Host string `default:"127.0.0.1" yaml:"host"`

internal/config/parser.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func (c *Config) UnmarshalYAML(v *yaml.Node) error {
1616
Endpoints []Endpoint `yaml:"endpoints"`
1717
ProjectDefaults ProjectSettings `yaml:"project_defaults"`
1818
Projects []yaml.Node `yaml:"projects"`
19+
Namespaces []yaml.Node `yaml:"namespaces"`
1920
HTTP HTTP `yaml:"http"`
2021
Log Log `yaml:"log"`
2122
}
@@ -43,12 +44,24 @@ func (c *Config) UnmarshalYAML(v *yaml.Node) error {
4344
}
4445

4546
if err := n.Decode(&p); err != nil {
46-
return nil
47+
return err
4748
}
4849

4950
c.Projects = append(c.Projects, p)
5051
}
5152

53+
for _, node := range _cfg.Namespaces {
54+
n := Namespace{
55+
ProjectSettings: c.ProjectDefaults,
56+
}
57+
58+
if err := node.Decode(&n); err != nil {
59+
return err
60+
}
61+
62+
c.Namespaces = append(c.Namespaces, n)
63+
}
64+
5265
return nil
5366
}
5467

internal/gitlab/gitlab.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gitlab
22

33
import (
4+
"fmt"
5+
"strconv"
46
"time"
57

68
durationpb "google.golang.org/protobuf/types/known/durationpb"
@@ -11,6 +13,17 @@ func ptr[T any](v T) *T {
1113
return &v
1214
}
1315

16+
func parseID(id interface{}) (string, error) {
17+
switch v := id.(type) {
18+
case int:
19+
return strconv.Itoa(v), nil
20+
case string:
21+
return v, nil
22+
default:
23+
return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id)
24+
}
25+
}
26+
1427
func convertTime(t *time.Time) *timestamppb.Timestamp {
1528
if t == nil {
1629
return nil

internal/gitlab/projects.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package gitlab
22

33
import (
44
"context"
5+
"fmt"
6+
"strings"
57

68
gitlab "github.com/xanzy/go-gitlab"
79

810
"github.com/cluttrdev/gitlab-exporter/protobuf/typespb"
911
)
1012

11-
type ListProjectOptions = gitlab.ListProjectsOptions
13+
type ListProjectsOptions = gitlab.ListProjectsOptions
14+
type ListGroupProjectsOptions = gitlab.ListGroupProjectsOptions
15+
type VisibilityValue = gitlab.VisibilityValue
1216

13-
func (c *Client) ListProjects(ctx context.Context, opt ListProjectOptions) ([]*typespb.Project, error) {
17+
func (c *Client) ListProjects(ctx context.Context, opt ListProjectsOptions) ([]*typespb.Project, error) {
1418
var projects []*typespb.Project
1519

1620
for {
@@ -37,14 +41,88 @@ func (c *Client) GetProject(ctx context.Context, id int64) (*typespb.Project, er
3741
Statistics: ptr(true),
3842
}
3943

40-
p, _, err := c.client.Projects.GetProject(int(id), &opt)
44+
p, _, err := c.client.Projects.GetProject(int(id), &opt, gitlab.WithContext(ctx))
4145
if err != nil {
4246
return nil, err
4347
}
4448

4549
return convertProject(p), nil
4650
}
4751

52+
type ListNamespaceProjectsOptions struct {
53+
gitlab.ListProjectsOptions
54+
55+
Kind string
56+
WithShared bool
57+
IncludeSubgroups bool
58+
}
59+
60+
func (c *Client) ListNamespaceProjects(ctx context.Context, id interface{}, opt ListNamespaceProjectsOptions) ([]*typespb.Project, error) {
61+
kind := strings.ToLower(opt.Kind)
62+
if !(strings.EqualFold(kind, "user") || strings.EqualFold(kind, "group")) {
63+
n, _, err := c.client.Namespaces.GetNamespace(id, gitlab.WithContext(ctx))
64+
if err != nil {
65+
return nil, fmt.Errorf("error determining namespace kind: %w", err)
66+
}
67+
kind = n.Kind
68+
}
69+
70+
if kind == "user" {
71+
return c.ListUserProjects(ctx, id, opt.ListProjectsOptions)
72+
} else if kind == "group" {
73+
return c.ListGroupProjects(ctx, id, gitlab.ListGroupProjectsOptions{
74+
ListOptions: opt.ListOptions,
75+
WithShared: &opt.WithShared,
76+
IncludeSubGroups: &opt.IncludeSubgroups,
77+
})
78+
}
79+
return nil, fmt.Errorf("invalid namespace kind: %v", kind)
80+
}
81+
82+
func (c *Client) ListUserProjects(ctx context.Context, uid interface{}, opt ListProjectsOptions) ([]*typespb.Project, error) {
83+
var projects []*typespb.Project
84+
85+
for {
86+
ps, resp, err := c.client.Projects.ListUserProjects(uid, &opt, gitlab.WithContext(ctx))
87+
if err != nil {
88+
return projects, err
89+
}
90+
91+
for _, p := range ps {
92+
projects = append(projects, convertProject(p))
93+
}
94+
95+
if resp.NextPage == 0 {
96+
break
97+
}
98+
opt.Page = resp.NextPage
99+
}
100+
101+
return projects, nil
102+
}
103+
104+
func (c *Client) ListGroupProjects(ctx context.Context, gid interface{}, opt ListGroupProjectsOptions) ([]*typespb.Project, error) {
105+
var projects []*typespb.Project
106+
107+
for {
108+
ps, resp, err := c.client.Groups.ListGroupProjects(gid, &opt, gitlab.WithContext(ctx))
109+
if err != nil {
110+
return projects, err
111+
}
112+
113+
for _, p := range ps {
114+
projects = append(projects, convertProject(p))
115+
}
116+
117+
if resp.NextPage == 0 {
118+
break
119+
}
120+
opt.Page = resp.NextPage
121+
}
122+
123+
return projects, nil
124+
}
125+
48126
func convertProject(p *gitlab.Project) *typespb.Project {
49127
return &typespb.Project{
50128
Id: int64(p.ID),

0 commit comments

Comments
 (0)