Skip to content

Commit da2db42

Browse files
committed
Tracker for production-readiness approvals
1 parent f70678f commit da2db42

File tree

8 files changed

+410
-15
lines changed

8 files changed

+410
-15
lines changed

cmd/kepval/main.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"k8s.io/enhancements/pkg/kepval/keps"
2525
)
2626

27+
// TODO: Can we remove this file?
28+
2729
func main() {
2830
os.Exit(run(os.Stderr))
2931
}
@@ -38,13 +40,10 @@ func run(w io.Writer) int {
3840
}
3941
defer file.Close()
4042
kep := parser.Parse(file)
41-
// if error is nil we can move on
42-
if kep.Error == nil {
43-
continue
43+
if kep.Error != nil {
44+
fmt.Fprintf(w, "%v has an error: %q\n", filename, kep.Error.Error())
45+
return 1
4446
}
45-
46-
fmt.Fprintf(w, "%v has an error: %q\n", filename, kep.Error.Error())
47-
return 1
4847
}
4948

5049
fmt.Fprintf(w, "No validation errors: %v\n", os.Args[1:])

cmd/kepval/main_test.go

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ limitations under the License.
1717
package main
1818

1919
import (
20-
"bytes"
2120
"os"
2221
"path/filepath"
2322
"strings"
2423
"testing"
24+
25+
"k8s.io/enhancements/pkg/kepval/keps"
26+
"k8s.io/enhancements/pkg/kepval/prrs"
2527
)
2628

2729
const (
2830
kepsDir = "keps"
31+
prrsDir = "keps/prod-readiness"
2932
kepMetadata = "kep.yaml"
3033
)
3134

@@ -70,14 +73,61 @@ func TestValidation(t *testing.T) {
7073
t.Fatal("must find more than 0 keps")
7174
}
7275

73-
// Overwrite the command line argument for the run() function
74-
os.Args = []string{"", ""}
75-
for _, file := range files {
76-
t.Run(file, func(t *testing.T) {
77-
os.Args[1] = file
78-
var b bytes.Buffer
79-
if exit := run(&b); exit != 0 {
80-
t.Fatalf("exit code was %d and not 0. Output:\n%s", exit, b.String())
76+
kepParser := &keps.Parser{}
77+
prrParser := &prrs.Parser{}
78+
prrsDir := filepath.Join("..", "..", prrsDir)
79+
80+
for _, filename := range files {
81+
t.Run(filename, func(t *testing.T) {
82+
kepFile, err := os.Open(filename)
83+
if err != nil {
84+
t.Fatalf("could not open file %s: %v\n", filename, err)
85+
}
86+
defer kepFile.Close()
87+
88+
kep := kepParser.Parse(kepFile)
89+
if kep.Error != nil {
90+
t.Errorf("%v has an error: %v", filename, kep.Error)
91+
}
92+
93+
requiredPRRApproval := len(kep.Number) > 0 && kep.LatestMilestone >= "v1.21"
94+
if !requiredPRRApproval {
95+
return
96+
}
97+
98+
prrFilename := filepath.Join(prrsDir, kep.OwningSIG, kep.Number)
99+
prrFile, err := os.Open(prrFilename)
100+
if os.IsNotExist(err) {
101+
t.Errorf("missing PRR Approval file under: %s", prrFilename)
102+
return
103+
}
104+
if err != nil {
105+
t.Fatalf("could not open file %s: %v\n", prrFilename, err)
106+
}
107+
prr := prrParser.Parse(prrFile)
108+
if prr.Error != nil {
109+
t.Errorf("PRR approval file %v has an error: %v", prrFilename, prr.Error)
110+
return
111+
}
112+
113+
var stageMilestone string
114+
var stagePRRApprover string
115+
switch kep.Stage {
116+
case "alpha":
117+
stageMilestone = kep.Milestone.Alpha
118+
stagePRRApprover = prr.Alpha.Approver
119+
case "beta":
120+
stageMilestone = kep.Milestone.Beta
121+
stagePRRApprover = prr.Beta.Approver
122+
case "stable":
123+
stageMilestone = kep.Milestone.Stable
124+
stagePRRApprover = prr.Stable.Approver
125+
}
126+
if len(stageMilestone) > 0 && stageMilestone >= "v1.21" {
127+
// PRR approval is needed.
128+
if len(stagePRRApprover) == 0 {
129+
t.Errorf("PRR not approved for: %s", kep.Stage)
130+
}
81131
}
82132
})
83133
}

keps/prod-readiness/OWNERS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Disable inheritance as this is production-readiness directory.
2+
options:
3+
no_parent_owners: true
4+
5+
approvers:
6+
- prod-readiness-approvers
7+
8+
# TODO: Tag PRs with PRR-related label.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kep-number: 1040
2+
beta:
3+
approver: "@wojtek-t"

pkg/kepval/prrs/approvals.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package prrs
18+
19+
import (
20+
"bufio"
21+
"bytes"
22+
"io"
23+
24+
"github.com/pkg/errors"
25+
"gopkg.in/yaml.v2"
26+
"k8s.io/enhancements/pkg/kepval/prrs/validations"
27+
)
28+
29+
type Approvals []*Approval
30+
31+
func (a *Approvals) AddApproval(approval *Approval) {
32+
*a = append(*a, approval)
33+
}
34+
35+
type Milestone struct {
36+
Approver string `json:"approver" yaml:"approver"`
37+
}
38+
39+
type Approval struct {
40+
Number string `json:"kep-number" yaml:"kep-number"`
41+
Alpha Milestone `json:"alpha" yaml:"alpha"`
42+
Beta Milestone `json:"beta" yaml:"beta"`
43+
Stable Milestone `json:"stable" yaml:"stable"`
44+
45+
Error error `json:"-" yaml:"-"`
46+
}
47+
48+
type Parser struct{}
49+
50+
func (p *Parser) Parse(in io.Reader) *Approval {
51+
scanner := bufio.NewScanner(in)
52+
var body bytes.Buffer
53+
for scanner.Scan() {
54+
line := scanner.Text() + "\n"
55+
body.WriteString(line)
56+
}
57+
58+
approval := &Approval{}
59+
if err := scanner.Err(); err != nil {
60+
approval.Error = errors.Wrap(err, "error reading file")
61+
return approval
62+
}
63+
64+
// First do structural checks
65+
test := map[interface{}]interface{}{}
66+
if err := yaml.Unmarshal(body.Bytes(), test); err != nil {
67+
approval.Error = errors.Wrap(err, "error unmarshaling YAML")
68+
return approval
69+
}
70+
if err := validations.ValidateStructure(test); err != nil {
71+
approval.Error = errors.Wrap(err, "error validating PRR approval metadata")
72+
return approval
73+
}
74+
75+
approval.Error = yaml.UnmarshalStrict(body.Bytes(), approval)
76+
return approval
77+
}

pkg/kepval/prrs/approvals_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package prrs
18+
19+
import (
20+
"strings"
21+
"testing"
22+
)
23+
24+
func TestValidParsing(t *testing.T) {
25+
testcases := []struct {
26+
name string
27+
fileContents string
28+
}{
29+
{
30+
"simple test",
31+
`
32+
kep-number: 12345
33+
beta:
34+
approver: "@wojtek-t"
35+
stable:
36+
approver: "johnbelamaric"
37+
`,
38+
},
39+
}
40+
for _, tc := range testcases {
41+
t.Run(tc.name, func(t *testing.T) {
42+
p := &Parser{}
43+
contents := strings.NewReader(tc.fileContents)
44+
out := p.Parse(contents)
45+
if out.Error != nil {
46+
t.Fatalf("expected no error but got one: %v", out.Error)
47+
}
48+
if out == nil {
49+
t.Fatal("out should not be nil")
50+
}
51+
})
52+
}
53+
}

pkg/kepval/prrs/validations/yaml.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package validations
18+
19+
import (
20+
"fmt"
21+
"sort"
22+
"strings"
23+
24+
"k8s.io/enhancements/pkg/kepval/util"
25+
)
26+
27+
var (
28+
mandatoryKeys = []string{"kep-number"}
29+
)
30+
31+
func ValidateStructure(parsed map[interface{}]interface{}) error {
32+
for _, key := range mandatoryKeys {
33+
if _, found := parsed[key]; !found {
34+
return util.NewKeyMustBeSpecified(key)
35+
}
36+
}
37+
38+
for key, value := range parsed {
39+
// First off the key has to be a string. fact.
40+
k, ok := key.(string)
41+
if !ok {
42+
return util.NewKeyMustBeString(k)
43+
}
44+
45+
// figure out the types
46+
switch strings.ToLower(k) {
47+
case "alpha", "beta", "stable":
48+
switch v := value.(type) {
49+
case map[interface{}]interface{}:
50+
if err := validateMilestone(v); err != nil {
51+
return fmt.Errorf("invalid %s field: %v", k, err)
52+
}
53+
case interface{}:
54+
return util.NewValueMustBeStruct(k, v)
55+
}
56+
}
57+
}
58+
return nil
59+
}
60+
61+
func validateMilestone(parsed map[interface{}]interface{}) error {
62+
// prrApprovers must be sorted to use SearchStrings down below...
63+
prrApprovers := util.PRRApprovers()
64+
sort.Strings(prrApprovers)
65+
66+
for key, value := range parsed {
67+
// First off the key has to be a string. fact.
68+
k, ok := key.(string)
69+
if !ok {
70+
return util.NewKeyMustBeString(k)
71+
}
72+
73+
// figure out the types
74+
switch strings.ToLower(k) {
75+
case "approver":
76+
switch v := value.(type) {
77+
case []interface{}:
78+
return util.NewValueMustBeString(k, v)
79+
}
80+
v, _ := value.(string)
81+
if len(v) > 0 && v[0] == '@' {
82+
// If "@" is appeneded at the beginning, remove it.
83+
v = v[1:]
84+
}
85+
index := sort.SearchStrings(prrApprovers, v)
86+
if index >= len(prrApprovers) || prrApprovers[index] != v {
87+
return util.NewValueMustBeOneOf(k, v, prrApprovers)
88+
}
89+
}
90+
}
91+
return nil
92+
}

0 commit comments

Comments
 (0)