Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions experimental/keptain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Simple KEP explorer website.

This is an experiment to see if we can make it easier for
maintainers to work with KEPs.

It doesn't do much yet, it is mostly setting up a framework
for us to start to put value-add ideas.

## Running

First, you should check out the KEPs repo:

```
git clone https://github.com/kubernetes/enhancements.git
```

Then, you can run the website:
```
go run .
```

Open your browser and go to [http://localhost:8080](http://localhost:8080)
17 changes: 17 additions & 0 deletions experimental/keptain/design/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Kubernetes KEP Explorer

This website is a simple website that allows the user to explore kubernetes KEPs.

We will start with basic "display" features,
and then add more features over time that streamline the KEP process,
for maintainers as well as for contributors.

## Features

### Basic Display Features

We should be able to display a list of KEPs,
and for each KEP we have a landing page that displays the KEP content.

Initially we link to the full KEP content from the landing page,
showing only keep metadata for each KEP.
13 changes: 13 additions & 0 deletions experimental/keptain/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module sigs.k8s.io/maintainers/experiments/keptain

go 1.23

toolchain go1.23.5

require (
github.com/yuin/goldmark v1.7.8
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/yaml v1.4.0
)

require github.com/go-logr/logr v1.4.1 // indirect
12 changes: 12 additions & 0 deletions experimental/keptain/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
29 changes: 29 additions & 0 deletions experimental/keptain/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"context"
"fmt"
"os"

"sigs.k8s.io/maintainers/experiments/keptain/pkg/store"
"sigs.k8s.io/maintainers/experiments/keptain/pkg/website"
)

func main() {
if err := run(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func run(ctx context.Context) error {
// Initialize the KEP repository
kepRepo, err := store.NewRepository("enhancements")
if err != nil {
return fmt.Errorf("error creating KEP repository: %w", err)
}

// Start the web server
server := website.NewServer(kepRepo)
return server.Run(":8080")
}
25 changes: 25 additions & 0 deletions experimental/keptain/pkg/model/kep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package model

// KEP represents a Kubernetes Enhancement Proposal
type KEP struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add Created to expose creation-date from the KEP spec ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Right now the order is random (golang maps). I also want to get "in progress" KEPs into the same view, so we have an updated-at timestamp. I would like to claim that I didn't add the date because I figure it will change when we add support for PRs, but that would be a lie :-)

// Path is the path to the KEP file, relative to the repo base
Path string `json:"path"`

// Title is the title of the KEP
Title string `json:"title"`

// Number is the number of the KEP
Number string `json:"number"`

// Authors are the authors of the KEP
Authors []string `json:"authors"`

// Status is the status of the KEP
Status string `json:"status"`

// TextURL is the URL to the KEP README.md file
TextURL string `json:"textURL"`

// TextContents is the contents of the KEP README.md file
TextContents string `json:"-"`
}
172 changes: 172 additions & 0 deletions experimental/keptain/pkg/store/kep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package store

import (
"fmt"
"os"
"path/filepath"
"strings"

"sigs.k8s.io/maintainers/experiments/keptain/pkg/model"
"sigs.k8s.io/yaml"
)

// Repository represents a KEP repository
type Repository struct {
basePath string
keps map[string]*model.KEP
}

// NewRepository creates a new KEP repository instance
func NewRepository(basePath string) (*Repository, error) {
r := &Repository{
basePath: basePath,
keps: make(map[string]*model.KEP),
}
if err := r.loadKEPs(); err != nil {
return nil, fmt.Errorf("error loading KEPs: %v", err)
}
return r, nil
}

func (r *Repository) loadKEPs() error {
// Walk the KEPs directory and load all KEPs
if err := filepath.Walk(r.basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}

// We assume there's a metadata file for each KEP called kep.yaml
if filepath.Base(path) != "kep.yaml" {
return nil
}

relativePath, err := filepath.Rel(r.basePath, path)
if err != nil {
return fmt.Errorf("error getting relative path: %w", err)
}

dir := filepath.Dir(path)
relativeDir := filepath.Dir(relativePath)

b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading KEP file: %w", err)
}
kep, err := r.parseKEPFile(b)
if err != nil {
// Log error but continue processing other KEPs
return fmt.Errorf("error parsing KEP %q: %w", path, err)
}

// use the (repo-relative) directory as the identifier for the KEP
kep.Path = relativeDir

// See if we have a README.md file
{
readme := filepath.Join(dir, "README.md")
readmeBytes, err := os.ReadFile(readme)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error getting README.md: %w", err)
}
return nil
}

if err == nil {
kep.TextContents = string(readmeBytes)
kep.TextURL = fmt.Sprintf("https://github.com/kubernetes/enhancements/blob/master/%s", filepath.Join(relativeDir, "README.md"))
}
}
r.keps[kep.Path] = kep
return nil
}); err != nil {
return fmt.Errorf("error walking KEPs: %w", err)
}

return nil
}

// ListKEPs returns all KEPs in the repository
// If query is provided, it will filter the KEPs based on the query
func (r *Repository) ListKEPs(query string) ([]*model.KEP, error) {
var ret []*model.KEP
for _, kep := range r.keps {
// Filter KEPs if search query is provided
match := true
if query != "" {
query = strings.ToLower(query)
if strings.Contains(strings.ToLower(kep.Title), query) ||
strings.Contains(strings.ToLower(kep.Number), query) ||
containsAuthor(kep.Authors, query) {
match = true
}
}

if match {
ret = append(ret, kep)
}
}
return ret, nil
}

func containsAuthor(authors []string, query string) bool {
for _, author := range authors {
if strings.Contains(strings.ToLower(author), query) {
return true
}
}
return false
}

// GetKEP returns a specific KEP by number
func (r *Repository) GetKEP(path string) (*model.KEP, error) {
kep, ok := r.keps[path]
if ok {
return kep, nil
}
return nil, fmt.Errorf("KEP %s not found", path)
}

// kepFile is the format used in the KEP file.
type kepFile struct {
Title string `json:"title"`
Number string `json:"kep-number"`
Authors []string `json:"authors"`
OwningSig string `json:"owning-sig"`
ParticipatingSigs []string `json:"participating-sigs"`
Reviewers []string `json:"reviewers"`
Approvers []string `json:"approvers"`
Editor string `json:"editor"`
CreationDate string `json:"creation-date"`
LastUpdated string `json:"last-updated"`
Status string `json:"status"`
SeeAlso []string `json:"see-also"`
Replaces []string `json:"replaces"`
SupersededBy []string `json:"superseded-by"`
}

// parseKEPFile parses a KEP yaml file
func (r *Repository) parseKEPFile(data []byte) (*model.KEP, error) {

var kep kepFile
if err := yaml.Unmarshal(data, &kep); err != nil {
return nil, fmt.Errorf("error parsing KEP yaml: %v", err)
}

// Extract additional metadata from the yaml
var rawMap map[string]interface{}
if err := yaml.Unmarshal(data, &rawMap); err != nil {
return nil, fmt.Errorf("error parsing KEP metadata: %v", err)
}

out := &model.KEP{
Title: kep.Title,
Number: kep.Number,
Authors: kep.Authors,
Status: kep.Status,
}
return out, nil
}
Loading