Skip to content

Commit b9fd328

Browse files
author
Alex Flom
authored
Merge pull request #31 from jpower432/feat/add-automated-config-validation
Feat/add automated config validation
2 parents 69d114d + 15a4c0b commit b9fd328

File tree

6 files changed

+875
-2
lines changed

6 files changed

+875
-2
lines changed
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Verify Peribolos
2+
on:
3+
push:
4+
branches:
5+
- '**'
6+
pull_request:
7+
branches:
8+
- main
9+
10+
jobs:
11+
project:
12+
name: Verify peribolos
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 20
15+
steps:
16+
- name: install Go
17+
uses: actions/setup-go@v3
18+
with:
19+
go-version: 1.18
20+
- name: checkout repo
21+
uses: actions/checkout@v3
22+
with:
23+
path: src/github.com/emporous/.github
24+
- name: verify go modules and vendor directory
25+
run: |
26+
go mod tidy
27+
go mod vendor
28+
working-directory: src/github.com/emporous/.github
29+
- name: running unit tests
30+
run: |
31+
go test ./... --config ../peribolos.yaml --owners-dir ../
32+
working-directory: src/github.com/emporous/.github

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
.idea
33
*.swp
44
.vscode
5+
6+
# Local vendor. Remove when ready to vendor dependencies.
7+
/vendor/

config/config_test.go

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
// These tests are adapted from https://github.com/kubernetes/org/blob/main/config/config_test.go
15+
16+
package config
17+
18+
import (
19+
"flag"
20+
"fmt"
21+
"io/ioutil"
22+
"os"
23+
"path"
24+
"sort"
25+
"strings"
26+
"testing"
27+
28+
"github.com/ghodss/yaml"
29+
"github.com/hmarr/codeowners"
30+
"k8s.io/apimachinery/pkg/util/sets"
31+
"k8s.io/test-infra/prow/config/org"
32+
"k8s.io/test-infra/prow/github"
33+
)
34+
35+
var configPath = flag.String("config", "config.yaml", "Path to peribolos config")
36+
var ownersDir = flag.String("owners-dir", ".", "Directory to CODEOWNERS")
37+
38+
var cfg org.FullConfig
39+
40+
func TestMain(m *testing.M) {
41+
flag.Parse()
42+
if *configPath == "" {
43+
fmt.Println("--config must be set")
44+
os.Exit(1)
45+
}
46+
47+
if *ownersDir == "" {
48+
fmt.Println("--owners-dir must be set")
49+
os.Exit(1)
50+
}
51+
52+
raw, err := ioutil.ReadFile(*configPath)
53+
if err != nil {
54+
fmt.Printf("cannot read configuation from %s: %v\n", *configPath, err)
55+
os.Exit(1)
56+
}
57+
58+
if err := yaml.Unmarshal(raw, &cfg); err != nil {
59+
fmt.Printf("cannot unmarshal configuration from %s: %v\n", *configPath, err)
60+
os.Exit(1)
61+
}
62+
63+
os.Exit(m.Run())
64+
}
65+
66+
func loadOwners(dir string) ([]string, error) {
67+
var owners []string
68+
69+
dir = path.Clean(dir)
70+
file, err := os.Open(path.Join(dir, "CODEOWNERS"))
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
ruleset, err := codeowners.ParseFile(file)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
rule, err := ruleset.Match(*configPath)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
if rule == nil {
86+
return nil, fmt.Errorf("no matching rule found for %s", *configPath)
87+
}
88+
89+
for _, owner := range rule.Owners {
90+
owners = append(owners, owner.String())
91+
}
92+
93+
return owners, nil
94+
}
95+
96+
func testDuplicates(list sets.Set[string]) error {
97+
found := sets.NewString()
98+
dups := sets.NewString()
99+
all := list.UnsortedList()
100+
for _, i := range all {
101+
if found.Has(i) {
102+
dups.Insert(i)
103+
}
104+
found.Insert(i)
105+
}
106+
if n := len(dups); n > 0 {
107+
return fmt.Errorf("%d duplicate names: %s", n, strings.Join(dups.List(), ", "))
108+
}
109+
return nil
110+
}
111+
112+
func isSorted(list []string) bool {
113+
items := make([]string, len(list))
114+
for _, l := range list {
115+
items = append(items, strings.ToLower(l))
116+
}
117+
118+
return sort.StringsAreSorted(items)
119+
}
120+
121+
func normalize(s sets.Set[string]) sets.Set[string] {
122+
out := sets.Set[string]{}
123+
for _, t := range s.UnsortedList() {
124+
out.Insert(github.NormLogin(t))
125+
}
126+
return out
127+
}
128+
129+
// testTeamMembers ensures that a user is not a maintainer and member at the same time,
130+
// there are no duplicate names in the list and all users are org members.
131+
func testTeamMembers(teams map[string]org.Team, admins sets.Set[string], orgMembers sets.Set[string], orgName string) []error {
132+
var errs []error
133+
for teamName, team := range teams {
134+
teamMaintainers := sets.New(team.Maintainers...)
135+
teamMembers := sets.New(team.Members...)
136+
137+
teamMaintainers = normalize(teamMaintainers)
138+
teamMembers = normalize(teamMembers)
139+
140+
// ensure all teams have privacy as closed
141+
if team.Privacy == nil || (team.Privacy != nil && *team.Privacy != org.Closed) {
142+
errs = append(errs, fmt.Errorf("The team %s in org %s doesn't have the `privacy: closed` field", teamName, orgName))
143+
}
144+
145+
// check for non-admins in maintainers list
146+
if nonAdminMaintainers := teamMaintainers.Difference(admins); len(nonAdminMaintainers) > 0 {
147+
errs = append(errs, fmt.Errorf("The team %s in org %s has non-admins listed as maintainers; these users should be in the members list instead: %s", teamName, orgName, strings.Join(nonAdminMaintainers.UnsortedList(), ",")))
148+
}
149+
150+
// check for users in both maintainers and members
151+
if both := teamMaintainers.Intersection(teamMembers); len(both) > 0 {
152+
errs = append(errs, fmt.Errorf("The team %s in org %s has users in both maintainer admin and member roles: %s", teamName, orgName, strings.Join(both.UnsortedList(), ", ")))
153+
}
154+
155+
// check for duplicates
156+
if err := testDuplicates(teamMaintainers); err != nil {
157+
errs = append(errs, fmt.Errorf("The team %s in org %s has duplicate maintainers: %v", teamName, orgName, err))
158+
}
159+
if err := testDuplicates(teamMembers); err != nil {
160+
errs = append(errs, fmt.Errorf("The team %s in org %s has duplicate members: %v", teamMembers, orgName, err))
161+
}
162+
163+
// check if all are org members
164+
if missing := teamMembers.Difference(orgMembers); len(missing) > 0 {
165+
errs = append(errs, fmt.Errorf("The following members of team %s are not %s org members: %s", teamName, orgName, strings.Join(missing.UnsortedList(), ", ")))
166+
}
167+
168+
// check if admins are a regular member of team
169+
if adminTeamMembers := teamMembers.Intersection(admins); len(adminTeamMembers) > 0 {
170+
errs = append(errs, fmt.Errorf("The team %s in org %s has org admins listed as members; these users should be in the maintainers list instead, and cannot be on the members list: %s", teamName, orgName, strings.Join(adminTeamMembers.UnsortedList(), ", ")))
171+
}
172+
173+
// check if lists are sorted
174+
if !isSorted(team.Maintainers) {
175+
errs = append(errs, fmt.Errorf("The team %s in org %s has an unsorted list of maintainers", teamName, orgName))
176+
}
177+
if !isSorted(team.Members) {
178+
errs = append(errs, fmt.Errorf("The team %s in org %s has an unsorted list of members", teamName, orgName))
179+
}
180+
181+
if team.Children != nil {
182+
errs = append(errs, testTeamMembers(team.Children, admins, orgMembers, orgName)...)
183+
}
184+
}
185+
return errs
186+
}
187+
188+
func TestOrgs(t *testing.T) {
189+
own, err := loadOwners(*ownersDir)
190+
if err != nil {
191+
t.Fatalf("failed to load CODEOWNERS: %v", err)
192+
}
193+
194+
for _, org := range cfg.Orgs {
195+
members := normalize(sets.New(org.Members...))
196+
admins := normalize(sets.New(org.Admins...))
197+
allOrgMembers := members.Union(admins)
198+
199+
approvers := normalize(sets.New(own...))
200+
201+
if diff := approvers.Difference(admins); len(diff) > 0 {
202+
t.Errorf("users do not match in CODEOWNERS and org admins '%s': %s", *org.Name, strings.Join(diff.UnsortedList(), ", "))
203+
}
204+
205+
if n := len(approvers); n < 4 {
206+
t.Errorf("Require at least 4 approvers, found %d: %s", n, strings.Join(approvers.UnsortedList(), ", "))
207+
}
208+
209+
if err := testDuplicates(approvers); err != nil {
210+
t.Errorf("duplicate approvers: %v", err)
211+
}
212+
213+
if both := admins.Intersection(members); len(both) > 0 {
214+
t.Errorf("users in both org admin and member roles for org '%s': %s", *org.Name, strings.Join(both.UnsortedList(), ", "))
215+
}
216+
217+
if err := testDuplicates(admins); err != nil {
218+
t.Errorf("duplicate admins: %v", err)
219+
}
220+
if err := testDuplicates(allOrgMembers); err != nil {
221+
t.Errorf("duplicate members: %v", err)
222+
}
223+
if !isSorted(org.Admins) {
224+
t.Errorf("admins for %s org are unsorted", *org.Name)
225+
}
226+
if !isSorted(org.Members) {
227+
t.Errorf("members for %s org are unsorted", *org.Name)
228+
}
229+
230+
if errs := testTeamMembers(org.Teams, admins, allOrgMembers, *org.Name); errs != nil {
231+
for _, err := range errs {
232+
t.Error(err)
233+
}
234+
}
235+
}
236+
237+
}

go.mod

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module github.com/emporous/.github
2+
3+
go 1.18
4+
5+
require (
6+
github.com/ghodss/yaml v1.0.0
7+
github.com/hmarr/codeowners v1.1.1
8+
k8s.io/apimachinery v0.26.1
9+
k8s.io/test-infra v0.0.0-20230128010633-10be4bc2e686
10+
)
11+
12+
require (
13+
github.com/beorn7/perks v1.0.1 // indirect
14+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
15+
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
16+
github.com/go-logr/logr v1.2.3 // indirect
17+
github.com/gogo/protobuf v1.3.2 // indirect
18+
github.com/golang/protobuf v1.5.2 // indirect
19+
github.com/gomodule/redigo v1.8.5 // indirect
20+
github.com/google/btree v1.0.1 // indirect
21+
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect
22+
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
23+
github.com/json-iterator/go v1.1.12 // indirect
24+
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
25+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
26+
github.com/modern-go/reflect2 v1.0.2 // indirect
27+
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
28+
github.com/prometheus/client_golang v1.12.1 // indirect
29+
github.com/prometheus/client_model v0.2.0 // indirect
30+
github.com/prometheus/common v0.32.1 // indirect
31+
github.com/prometheus/procfs v0.7.3 // indirect
32+
github.com/shurcooL/githubv4 v0.0.0-20210725200734-83ba7b4c9228 // indirect
33+
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
34+
github.com/sirupsen/logrus v1.8.1 // indirect
35+
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
36+
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb // indirect
37+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
38+
golang.org/x/sys v0.3.0 // indirect
39+
golang.org/x/text v0.5.0 // indirect
40+
google.golang.org/appengine v1.6.7 // indirect
41+
google.golang.org/protobuf v1.28.1 // indirect
42+
gopkg.in/inf.v0 v0.9.1 // indirect
43+
gopkg.in/yaml.v2 v2.4.0 // indirect
44+
k8s.io/klog/v2 v2.80.1 // indirect
45+
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
46+
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
47+
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
48+
sigs.k8s.io/yaml v1.3.0 // indirect
49+
)

0 commit comments

Comments
 (0)