Skip to content

Commit a7a3f1d

Browse files
committed
Add the query subcommand to kepctl
1 parent 90f0e00 commit a7a3f1d

File tree

13 files changed

+383
-10
lines changed

13 files changed

+383
-10
lines changed

cmd/kepctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ func buildMainCommand() (*cobra.Command, error) {
4949

5050
rootCmd.AddCommand(buildCreateCommand(k))
5151
rootCmd.AddCommand(buildPromoteCommand(k))
52+
rootCmd.AddCommand(buildQueryCommand(k))
5253
return rootCmd, nil
5354
}

cmd/kepctl/query.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"k8s.io/enhancements/pkg/kepctl"
6+
)
7+
8+
func buildQueryCommand(k *kepctl.Client) *cobra.Command {
9+
opts := kepctl.QueryOpts{}
10+
cmd := &cobra.Command{
11+
Use: "query",
12+
Short: "Query KEPs",
13+
Long: "Query the local filesystem, and optionally GitHub PRs for KEPs",
14+
Example: ` kepctl query --sig architecture --status provisional --include-prs`,
15+
PreRunE: func(cmd *cobra.Command, args []string) error {
16+
return opts.Validate(args)
17+
},
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
return k.Query(opts)
20+
},
21+
}
22+
23+
f := cmd.Flags()
24+
f.StringSliceVar(&opts.SIG, "sig", nil, "SIG")
25+
f.StringSliceVar(&opts.Status, "status", nil, "Status")
26+
f.StringSliceVar(&opts.Stage, "stage", nil, "Stage")
27+
f.BoolVar(&opts.IncludePRs, "include-prs", false, "Include PRs in the results")
28+
29+
addRepoPathFlag(f, &opts.CommonArgs)
30+
31+
return cmd
32+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module k8s.io/enhancements
33
go 1.12
44

55
require (
6+
github.com/google/go-github/v32 v32.1.0
67
github.com/pkg/errors v0.8.0
78
github.com/spf13/cobra v1.0.0
89
github.com/spf13/pflag v1.0.3

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
3333
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
3434
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3535
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
36+
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3637
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
3738
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
39+
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
40+
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
41+
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
42+
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
43+
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
3844
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
3945
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
4046
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -100,6 +106,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
100106
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
101107
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
102108
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
109+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
103110
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
104111
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
105112
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

pkg/kepctl/kepctl.go

Lines changed: 158 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ package kepctl
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"fmt"
2223
"io"
2324
"io/ioutil"
2425
"os"
2526
"path/filepath"
2627
"regexp"
28+
"strings"
2729

30+
"github.com/google/go-github/v32/github"
2831
"gopkg.in/yaml.v2"
2932
"k8s.io/enhancements/pkg/kepval/keps"
3033
)
@@ -139,23 +142,102 @@ func validateKEP(p *keps.Proposal) error {
139142
return nil
140143
}
141144

145+
func findLocalKEPs(repoPath string, sig string) ([]string, error) {
146+
rootPath := filepath.Join(
147+
repoPath,
148+
"keps",
149+
sig)
150+
151+
keps := []string{}
152+
err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
153+
if err != nil {
154+
return err
155+
}
156+
if !info.Mode().IsRegular() {
157+
return nil
158+
}
159+
if info.Name() == "kep.yaml" {
160+
keps = append(keps, filepath.Base(filepath.Dir(path)))
161+
return filepath.SkipDir
162+
}
163+
if filepath.Ext(path) != ".md" {
164+
return nil
165+
}
166+
if info.Name() == "README.md" {
167+
return nil
168+
}
169+
keps = append(keps, info.Name()[0:len(info.Name())-3])
170+
return nil
171+
})
172+
173+
return keps, err
174+
}
175+
176+
func (c *Client) findPRKEPs(sig string) (*keps.Proposal, error) {
177+
gh := github.NewClient(nil)
178+
pulls, _, err := gh.PullRequests.List(context.Background(), "kubernetes", "enhancements", &github.PullRequestListOptions{})
179+
if err != nil {
180+
return nil, err
181+
}
182+
183+
for _, pr := range pulls {
184+
foundKind, foundSIG := false, false
185+
sigLabel := strings.Replace(sig, "-", "/", 1)
186+
for _, l := range pr.Labels {
187+
if *l.Name == "kind/kep" {
188+
foundKind = true
189+
}
190+
if *l.Name == sigLabel {
191+
foundSIG = true
192+
}
193+
}
194+
if !foundKind || !foundSIG {
195+
continue
196+
}
197+
}
198+
199+
return nil, nil
200+
}
201+
142202
func (c *Client) readKEP(repoPath string, sig, name string) (*keps.Proposal, error) {
143203
kepPath := filepath.Join(
144204
repoPath,
145205
"keps",
146206
sig,
147207
name,
148208
"kep.yaml")
149-
b, err := ioutil.ReadFile(kepPath)
150-
if err != nil {
151-
return nil, fmt.Errorf("unable to read KEP metadata: %s", err)
209+
_, err := os.Stat(kepPath)
210+
if err == nil {
211+
b, err := ioutil.ReadFile(kepPath)
212+
if err != nil {
213+
return nil, fmt.Errorf("unable to read KEP metadata: %s", err)
214+
}
215+
var p keps.Proposal
216+
err = yaml.Unmarshal(b, &p)
217+
if err != nil {
218+
return nil, fmt.Errorf("unable to load KEP metadata: %s", err)
219+
}
220+
return &p, nil
152221
}
153-
var p keps.Proposal
154-
err = yaml.Unmarshal(b, &p)
222+
223+
// No kep.yaml, treat as old-style KEP
224+
kepPath = filepath.Join(
225+
repoPath,
226+
"keps",
227+
sig,
228+
name) + ".md"
229+
b, err := ioutil.ReadFile(kepPath)
155230
if err != nil {
156231
return nil, fmt.Errorf("unable to load KEP metadata: %s", err)
157232
}
158-
return &p, nil
233+
r := bytes.NewReader(b)
234+
parser := &keps.Parser{}
235+
236+
kep := parser.Parse(r)
237+
if kep.Error != nil {
238+
return nil, fmt.Errorf("kep is invalid: %s", kep.Error)
239+
}
240+
return kep, nil
159241
}
160242

161243
func (c *Client) writeKEP(kep *keps.Proposal, opts CommonArgs) error {
@@ -181,3 +263,73 @@ func (c *Client) writeKEP(kep *keps.Proposal, opts CommonArgs) error {
181263
fmt.Fprintf(c.Out, "writing KEP to %s\n", newPath)
182264
return ioutil.WriteFile(newPath, b, os.ModePerm)
183265
}
266+
267+
type PrintConfig interface {
268+
Title() string
269+
Format() string
270+
Value(*keps.Proposal) string
271+
}
272+
273+
type printConfig struct {
274+
title string
275+
format string
276+
valueFunc func(*keps.Proposal) string
277+
}
278+
279+
func (p *printConfig) Title() string { return p.title }
280+
func (p *printConfig) Format() string { return p.format }
281+
func (p *printConfig) Value(k *keps.Proposal) string {
282+
return p.valueFunc(k)
283+
}
284+
285+
var dfltConfig = map[string]printConfig{
286+
"Authors": {"Authors", "%-30s", func(k *keps.Proposal) string { return strings.Join(k.Authors, ", ") }},
287+
"LastUpdated": {"Updated", "%-10s", func(k *keps.Proposal) string { return k.LastUpdated }},
288+
"SIG": {"SIG", "%-12s", func(k *keps.Proposal) string {
289+
if strings.HasPrefix(k.OwningSIG, "sig-") {
290+
return k.OwningSIG[4:]
291+
} else {
292+
return k.OwningSIG
293+
}
294+
}},
295+
"Stage": {"Stage", "%-6s", func(k *keps.Proposal) string { return k.Stage }},
296+
"Status": {"Status", "%-16s", func(k *keps.Proposal) string { return k.Status }},
297+
"Title": {"Title", "%-30s", func(k *keps.Proposal) string { return k.Title }},
298+
}
299+
300+
func DefaultPrintConfigs(names ...string) []PrintConfig {
301+
var configs []PrintConfig
302+
for _, n := range names {
303+
// copy to allow it to be tweaked by the caller
304+
c := dfltConfig[n]
305+
configs = append(configs, &c)
306+
}
307+
return configs
308+
}
309+
310+
func (c *Client) PrintTable(configs []PrintConfig, proposals []*keps.Proposal) {
311+
if len(configs) == 0 {
312+
return
313+
}
314+
315+
fstr := configs[0].Format()
316+
for _, c := range configs[1:] {
317+
fstr += " " + c.Format()
318+
}
319+
fstr += "\n"
320+
321+
fmt.Fprintf(c.Out, fstr, mapPrintConfigs(configs, func(p PrintConfig) string { return p.Title() })...)
322+
fmt.Fprintf(c.Out, fstr, mapPrintConfigs(configs, func(p PrintConfig) string { return strings.Repeat("-", len(p.Title())) })...)
323+
324+
for _, k := range proposals {
325+
fmt.Fprintf(c.Out, fstr, mapPrintConfigs(configs, func(p PrintConfig) string { return p.Value(k) })...)
326+
}
327+
}
328+
329+
func mapPrintConfigs(configs []PrintConfig, mapFunc func(p PrintConfig) string) []interface{} {
330+
var s []interface{}
331+
for _, c := range configs {
332+
s = append(s, mapFunc(c))
333+
}
334+
return s
335+
}

pkg/kepctl/kepctl_test.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package kepctl
1818

1919
import (
20+
"fmt"
2021
"io/ioutil"
2122
"testing"
2223

@@ -30,16 +31,17 @@ func TestValidate(t *testing.T) {
3031
testcases := []struct {
3132
name string
3233
file string
33-
err string
34+
err error
3435
}{
3536
{
3637
name: "valid kep passes valdiate",
3738
file: "testdata/valid-kep.yaml",
39+
err: nil,
3840
},
3941
{
4042
name: "invalid kep fails valdiate for owning-sig",
4143
file: "testdata/invalid-kep.yaml",
42-
err: "but it is a string: sig-awesome",
44+
err: fmt.Errorf(`kep is invalid: error validating KEP metadata: "owning-sig" must be one of (committee-code-of-conduct,committee-product-security,committee-steering,sig-api-machinery,sig-apps,sig-architecture,sig-auth,sig-autoscaling,sig-cli,sig-cloud-provider,sig-cluster-lifecycle,sig-contributor-experience,sig-docs,sig-instrumentation,sig-multicluster,sig-network,sig-node,sig-release,sig-scalability,sig-scheduling,sig-service-catalog,sig-storage,sig-testing,sig-ui,sig-usability,sig-windows,ug-big-data,ug-vmware-users,wg-api-expression,wg-component-standard,wg-data-protection,wg-iot-edge,wg-k8s-infra,wg-lts,wg-machine-learning,wg-multitenancy,wg-policy,wg-security-audit) but it is a string: sig-awesome`),
4345
},
4446
}
4547

@@ -51,12 +53,46 @@ func TestValidate(t *testing.T) {
5153
err = yaml.Unmarshal(b, &p)
5254
require.NoError(t, err)
5355
err = validateKEP(&p)
54-
if tc.err == "" {
56+
if tc.err == nil {
5557
assert.NoError(t, err)
5658
} else {
57-
assert.Contains(t, err.Error(), tc.err)
59+
assert.EqualError(t, err, tc.err.Error())
5860
}
5961

6062
})
6163
}
6264
}
65+
66+
func TestFindLocalKEPs(t *testing.T) {
67+
testcases := []struct {
68+
sig string
69+
keps []string
70+
}{
71+
{
72+
"sig-architecture",
73+
[]string{"123-newstyle", "20200115-kubectl-diff"},
74+
},
75+
{
76+
"sig-sig",
77+
[]string{},
78+
},
79+
}
80+
81+
for i, tc := range testcases {
82+
k, err := findLocalKEPs("testdata", tc.sig)
83+
if err != nil {
84+
t.Errorf("Test case %d: expected no error but got %s", i, err)
85+
continue
86+
}
87+
if len(k) != len(tc.keps) {
88+
t.Errorf("Test case %d: expected %s but got %s", i, tc.keps, k)
89+
continue
90+
}
91+
for j, kn := range k {
92+
if kn != tc.keps[j] {
93+
t.Errorf("Test case %d: expected %s but got %s", i, tc.keps[j], kn)
94+
}
95+
}
96+
}
97+
findLocalKEPs("testdata", "sig-architecture")
98+
}

0 commit comments

Comments
 (0)