Skip to content

Commit 8314507

Browse files
committed
Initial spike on a tool to help maintainers with KEPs
We create a simple website for navigating KEPs. This is really just a first step, to give us somewhere to put advanced features later.
1 parent d37e758 commit 8314507

File tree

11 files changed

+887
-0
lines changed

11 files changed

+887
-0
lines changed

experimental/keptain/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Simple KEP explorer website.
2+
3+
This is an experiment to see if we can make it easier for
4+
maintainers to work with KEPs.
5+
6+
It doesn't do much yet, it is mostly setting up a framework
7+
for us to start to put value-add ideas.
8+
9+
## Running
10+
11+
First, you should check out the KEPs repo:
12+
13+
```
14+
git clone https://github.com/kubernetes/enhancements.git
15+
```
16+
17+
Then, you can run the website:
18+
```
19+
go run .
20+
```
21+
22+
Open your browser and go to [http://localhost:8080](http://localhost:8080)

experimental/keptain/design/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Kubernetes KEP Explorer
2+
3+
This website is a simple website that allows the user to explore kubernetes KEPs.
4+
5+
We will start with basic "display" features,
6+
and then add more features over time that streamline the KEP process,
7+
for maintainers as well as for contributors.
8+
9+
## Features
10+
11+
### Basic Display Features
12+
13+
We should be able to display a list of KEPs,
14+
and for each KEP we have a landing page that displays the KEP content.
15+
16+
Initially we link to the full KEP content from the landing page,
17+
showing only keep metadata for each KEP.

experimental/keptain/go.mod

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module sigs.k8s.io/maintainers/experiments/keptain
2+
3+
go 1.23
4+
5+
toolchain go1.23.5
6+
7+
require (
8+
github.com/yuin/goldmark v1.7.8
9+
k8s.io/klog/v2 v2.130.1
10+
sigs.k8s.io/yaml v1.4.0
11+
)
12+
13+
require github.com/go-logr/logr v1.4.1 // indirect

experimental/keptain/go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
2+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
4+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
6+
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
10+
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
11+
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
12+
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

experimental/keptain/main.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"sigs.k8s.io/maintainers/experiments/keptain/pkg/store"
9+
"sigs.k8s.io/maintainers/experiments/keptain/pkg/website"
10+
)
11+
12+
func main() {
13+
if err := run(context.Background()); err != nil {
14+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
15+
os.Exit(1)
16+
}
17+
}
18+
19+
func run(ctx context.Context) error {
20+
// Initialize the KEP repository
21+
kepRepo, err := store.NewRepository("enhancements")
22+
if err != nil {
23+
return fmt.Errorf("error creating KEP repository: %w", err)
24+
}
25+
26+
// Start the web server
27+
server := website.NewServer(kepRepo)
28+
return server.Run(":8080")
29+
}

experimental/keptain/pkg/model/kep.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package model
2+
3+
// KEP represents a Kubernetes Enhancement Proposal
4+
type KEP struct {
5+
// Path is the path to the KEP file, relative to the repo base
6+
Path string `json:"path"`
7+
8+
// Title is the title of the KEP
9+
Title string `json:"title"`
10+
11+
// Number is the number of the KEP
12+
Number string `json:"number"`
13+
14+
// Authors are the authors of the KEP
15+
Authors []string `json:"authors"`
16+
17+
// Status is the status of the KEP
18+
Status string `json:"status"`
19+
20+
// TextURL is the URL to the KEP README.md file
21+
TextURL string `json:"textURL"`
22+
23+
// TextContents is the contents of the KEP README.md file
24+
TextContents string `json:"-"`
25+
}

experimental/keptain/pkg/store/kep.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package store
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"sigs.k8s.io/maintainers/experiments/keptain/pkg/model"
10+
"sigs.k8s.io/yaml"
11+
)
12+
13+
// Repository represents a KEP repository
14+
type Repository struct {
15+
basePath string
16+
keps map[string]*model.KEP
17+
}
18+
19+
// NewRepository creates a new KEP repository instance
20+
func NewRepository(basePath string) (*Repository, error) {
21+
r := &Repository{
22+
basePath: basePath,
23+
keps: make(map[string]*model.KEP),
24+
}
25+
if err := r.loadKEPs(); err != nil {
26+
return nil, fmt.Errorf("error loading KEPs: %v", err)
27+
}
28+
return r, nil
29+
}
30+
31+
func (r *Repository) loadKEPs() error {
32+
// Walk the KEPs directory and load all KEPs
33+
if err := filepath.Walk(r.basePath, func(path string, info os.FileInfo, err error) error {
34+
if err != nil {
35+
return err
36+
}
37+
if info.IsDir() {
38+
return nil
39+
}
40+
41+
// We assume there's a metadata file for each KEP called kep.yaml
42+
if filepath.Base(path) != "kep.yaml" {
43+
return nil
44+
}
45+
46+
relativePath, err := filepath.Rel(r.basePath, path)
47+
if err != nil {
48+
return fmt.Errorf("error getting relative path: %w", err)
49+
}
50+
51+
dir := filepath.Dir(path)
52+
relativeDir := filepath.Dir(relativePath)
53+
54+
b, err := os.ReadFile(path)
55+
if err != nil {
56+
return fmt.Errorf("error reading KEP file: %w", err)
57+
}
58+
kep, err := r.parseKEPFile(b)
59+
if err != nil {
60+
// Log error but continue processing other KEPs
61+
return fmt.Errorf("error parsing KEP %q: %w", path, err)
62+
}
63+
64+
// use the (repo-relative) directory as the identifier for the KEP
65+
kep.Path = relativeDir
66+
67+
// See if we have a README.md file
68+
{
69+
readme := filepath.Join(dir, "README.md")
70+
readmeBytes, err := os.ReadFile(readme)
71+
if err != nil {
72+
if !os.IsNotExist(err) {
73+
return fmt.Errorf("error getting README.md: %w", err)
74+
}
75+
return nil
76+
}
77+
78+
if err == nil {
79+
kep.TextContents = string(readmeBytes)
80+
kep.TextURL = fmt.Sprintf("https://github.com/kubernetes/enhancements/blob/master/%s", filepath.Join(relativeDir, "README.md"))
81+
}
82+
}
83+
r.keps[kep.Path] = kep
84+
return nil
85+
}); err != nil {
86+
return fmt.Errorf("error walking KEPs: %w", err)
87+
}
88+
89+
return nil
90+
}
91+
92+
// ListKEPs returns all KEPs in the repository
93+
// If query is provided, it will filter the KEPs based on the query
94+
func (r *Repository) ListKEPs(query string) ([]*model.KEP, error) {
95+
var ret []*model.KEP
96+
for _, kep := range r.keps {
97+
// Filter KEPs if search query is provided
98+
match := true
99+
if query != "" {
100+
query = strings.ToLower(query)
101+
if strings.Contains(strings.ToLower(kep.Title), query) ||
102+
strings.Contains(strings.ToLower(kep.Number), query) ||
103+
containsAuthor(kep.Authors, query) {
104+
match = true
105+
}
106+
}
107+
108+
if match {
109+
ret = append(ret, kep)
110+
}
111+
}
112+
return ret, nil
113+
}
114+
115+
func containsAuthor(authors []string, query string) bool {
116+
for _, author := range authors {
117+
if strings.Contains(strings.ToLower(author), query) {
118+
return true
119+
}
120+
}
121+
return false
122+
}
123+
124+
// GetKEP returns a specific KEP by number
125+
func (r *Repository) GetKEP(path string) (*model.KEP, error) {
126+
kep, ok := r.keps[path]
127+
if ok {
128+
return kep, nil
129+
}
130+
return nil, fmt.Errorf("KEP %s not found", path)
131+
}
132+
133+
// kepFile is the format used in the KEP file.
134+
type kepFile struct {
135+
Title string `json:"title"`
136+
Number string `json:"kep-number"`
137+
Authors []string `json:"authors"`
138+
OwningSig string `json:"owning-sig"`
139+
ParticipatingSigs []string `json:"participating-sigs"`
140+
Reviewers []string `json:"reviewers"`
141+
Approvers []string `json:"approvers"`
142+
Editor string `json:"editor"`
143+
CreationDate string `json:"creation-date"`
144+
LastUpdated string `json:"last-updated"`
145+
Status string `json:"status"`
146+
SeeAlso []string `json:"see-also"`
147+
Replaces []string `json:"replaces"`
148+
SupersededBy []string `json:"superseded-by"`
149+
}
150+
151+
// parseKEPFile parses a KEP yaml file
152+
func (r *Repository) parseKEPFile(data []byte) (*model.KEP, error) {
153+
154+
var kep kepFile
155+
if err := yaml.Unmarshal(data, &kep); err != nil {
156+
return nil, fmt.Errorf("error parsing KEP yaml: %v", err)
157+
}
158+
159+
// Extract additional metadata from the yaml
160+
var rawMap map[string]interface{}
161+
if err := yaml.Unmarshal(data, &rawMap); err != nil {
162+
return nil, fmt.Errorf("error parsing KEP metadata: %v", err)
163+
}
164+
165+
out := &model.KEP{
166+
Title: kep.Title,
167+
Number: kep.Number,
168+
Authors: kep.Authors,
169+
Status: kep.Status,
170+
}
171+
return out, nil
172+
}

0 commit comments

Comments
 (0)