Skip to content

Commit 4d4acc5

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 4d4acc5

File tree

11 files changed

+866
-0
lines changed

11 files changed

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

0 commit comments

Comments
 (0)