1
2
3
4 package team
5
6 import (
7 "fmt"
8 "io/fs"
9 "path/filepath"
10 "sort"
11 "strings"
12
13 "sigs.k8s.io/yaml"
14 )
15
16
17 type Team struct {
18
19 Name string `json:"name,omitempty"`
20
21 Leads []string `json:"leads,omitempty"`
22 Members []string `json:"members,omitempty"`
23
24
25 Teams []*Team `json:"teams,omitempty"`
26
27
28 Charter string `json:"charter,omitempty"`
29
30
31 Mission string `json:"mission_statement,omitempty"`
32
33
34 Contact *Contact `json:"contact,omitempty"`
35
36
37 Slack []Slack `json:"slack,omitempty"`
38
39
40 Meetings []Meeting `json:"meetings,omitempty"`
41
42
43
44 Links map[string]string `json:"links,omitempty"`
45
46
47
48 File string `json:"file,omitempty"`
49
50 parent *Team
51 }
52
53 type Contact struct {
54 Slack Slack `json:"slack,omitempty"`
55 }
56
57 type Slack struct {
58 Name string `json:"name"`
59 Link string `json:"link"`
60 }
61
62 type Meeting struct {
63 Description string `json:"description,omitempty"`
64 Day string `json:"day,omitempty"`
65 Time string `json:"time,omitempty"`
66 TZ string `json:"tz,omitempty"`
67 Frequency string `json:"frequency,omitempty"`
68 URL string `json:"url,omitempty"`
69 Agenda string `json:"agenda,omitempty"`
70 Minutes string `json:"minutes,omitempty"`
71 }
72
73 func (t *Team) IsValid() error {
74 if t.File != "" {
75
76 if t.Name != "" || t.Teams != nil || t.Leads != nil || t.Members != nil ||
77 t.Charter != "" || t.Mission != "" || t.Contact != nil ||
78 t.Slack != nil || t.Meetings != nil {
79 return fmt.Errorf("can't provide team configuration and 'file', all " +
80 "team config must come from referenced file if present")
81 }
82
83 return nil
84 }
85
86 if t.Name == "" {
87 return fmt.Errorf("'name' is required")
88 }
89
90 if err := isUniq(t.Leads); err != nil {
91 return fmt.Errorf("leads has duplicate participants: %w", err)
92 }
93
94 if err := isUniq(t.Members); err != nil {
95 return fmt.Errorf("members has duplicate participants: %w", err)
96 }
97
98 for _, l := range t.Leads {
99 for _, m := range t.Members {
100 if l == m {
101 return fmt.Errorf("lead %s is also specified as a member", l)
102 }
103 }
104 }
105
106 for _, child := range t.Teams {
107 if err := child.IsValid(); err != nil {
108 return fmt.Errorf("%s 'teams' invalid: %w", t.Name, err)
109 }
110 }
111
112 return nil
113 }
114
115 func (t *Team) HasParent() bool {
116 return t.parent != nil
117 }
118
119 func (t *Team) Parent() *Team {
120 return t.parent
121 }
122
123
124
125
126
127 func (t *Team) AllMembers() []string {
128 if len(t.Teams) == 0 {
129 return t.Members
130 }
131 m := append([]string{}, t.Members...)
132 for _, c := range t.Teams {
133 m = append(m, c.AllParticipants()...)
134 }
135 return dedupe(m)
136 }
137
138
139
140 func (t *Team) AllParticipants() []string {
141 p := t.Participants()
142 for _, c := range t.Teams {
143 p = append(p, c.AllParticipants()...)
144 }
145 return dedupe(p)
146 }
147
148
149
150 func (t *Team) Participants() []string {
151 return append(t.Leads, t.Members...)
152 }
153
154
155
156 func Load(fsys fs.FS, path string) (*Team, error) {
157 stat, err := fs.Stat(fsys, path)
158 if err != nil {
159 return nil, err
160 }
161 if stat.IsDir() {
162 path = filepath.Join(path, "teams.yaml")
163 }
164
165 return load(fsys, path)
166 }
167
168 func load(fsys fs.FS, path string) (*Team, error) {
169 data, err := fs.ReadFile(fsys, path)
170 if err != nil {
171 return nil, err
172 }
173
174 t := &Team{}
175 if err := yaml.Unmarshal(data, t); err != nil {
176 return nil, err
177 }
178
179 normalize(t)
180
181 if err := t.IsValid(); err != nil {
182 return nil, fmt.Errorf("%s is invalid: %w", path, err)
183 }
184
185 if t.File != "" {
186 return load(fsys, filepath.Join(filepath.Dir(path), t.File))
187 }
188
189 t.Teams, err = loadTeams(fsys, filepath.Dir(path), t.Teams)
190 if err != nil {
191 return nil, err
192 }
193
194 for i := range t.Teams {
195 t.Teams[i].parent = t
196 }
197
198 return t, nil
199 }
200
201 func loadTeams(fsys fs.FS, dir string, tt []*Team) ([]*Team, error) {
202 for i := range tt {
203 if tt[i].File != "" {
204 var err error
205 p := filepath.Join(dir, tt[i].File)
206 tt[i], err = load(fsys, p)
207 if err != nil {
208 return nil, err
209 }
210 }
211 if len(tt[i].Teams) > 0 {
212 var err error
213 tt[i].Teams, err = loadTeams(fsys, dir, tt[i].Teams)
214 if err != nil {
215 return nil, err
216 }
217 for j := range tt[i].Teams {
218 tt[i].Teams[j].parent = tt[i]
219 }
220 }
221 }
222 return tt, nil
223 }
224
225 func normalize(t *Team) {
226
227 for i := range t.Leads {
228 t.Leads[i] = strings.ToLower(t.Leads[i])
229 }
230 for i := range t.Members {
231 t.Members[i] = strings.ToLower(t.Members[i])
232 }
233 }
234
235 func dedupe(s []string) []string {
236 sort.Strings(s)
237 j := 0
238 for i := 1; i < len(s); i++ {
239 if s[j] == s[i] {
240 continue
241 }
242 j++
243 s[j] = s[i]
244 }
245 result := s[:j+1]
246 return result
247 }
248
249 func isUniq(s []string) error {
250 sort.Strings(s)
251 j := 0
252 for i := 1; i < len(s); i++ {
253 if s[j] == s[i] {
254 return fmt.Errorf("%s appears twice", s[j])
255 }
256 }
257 return nil
258 }
259
View as plain text