diff --git a/experimental/keptain/README.md b/experimental/keptain/README.md new file mode 100644 index 0000000..a5ec232 --- /dev/null +++ b/experimental/keptain/README.md @@ -0,0 +1,29 @@ +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 need to fetch the pull requests: + +``` +cd enhancements +git fetch origin '+refs/pull/*:refs/pull/*' +``` + +Then, you can run the website: +``` +go run . +``` + +Open your browser and go to [http://localhost:8080](http://localhost:8080) \ No newline at end of file diff --git a/experimental/keptain/design/README.md b/experimental/keptain/design/README.md new file mode 100644 index 0000000..a50e279 --- /dev/null +++ b/experimental/keptain/design/README.md @@ -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. diff --git a/experimental/keptain/go.mod b/experimental/keptain/go.mod new file mode 100644 index 0000000..0809159 --- /dev/null +++ b/experimental/keptain/go.mod @@ -0,0 +1,40 @@ +module sigs.k8s.io/maintainers/experiments/keptain + +go 1.23 + +toolchain go1.23.5 + +require ( + github.com/go-git/go-git/v5 v5.13.2 + github.com/google/go-github/v69 v69.2.0 + github.com/yuin/goldmark v1.7.8 + k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/experimental/keptain/go.sum b/experimental/keptain/go.sum new file mode 100644 index 0000000..7d72b1e --- /dev/null +++ b/experimental/keptain/go.sum @@ -0,0 +1,123 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= diff --git a/experimental/keptain/main.go b/experimental/keptain/main.go new file mode 100644 index 0000000..a76ce5c --- /dev/null +++ b/experimental/keptain/main.go @@ -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(ctx, "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") +} diff --git a/experimental/keptain/pkg/model/kep.go b/experimental/keptain/pkg/model/kep.go new file mode 100644 index 0000000..03754d5 --- /dev/null +++ b/experimental/keptain/pkg/model/kep.go @@ -0,0 +1,25 @@ +package model + +// KEP represents a Kubernetes Enhancement Proposal +type KEP struct { + // 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:"-"` +} diff --git a/experimental/keptain/pkg/model/pullrequest.go b/experimental/keptain/pkg/model/pullrequest.go new file mode 100644 index 0000000..97fb469 --- /dev/null +++ b/experimental/keptain/pkg/model/pullrequest.go @@ -0,0 +1,28 @@ +package model + +import "fmt" + +// PullRequest is a pull request +type PullRequest struct { + // The number of the PR + Number int + + // The title of the PR + Title string + + // The author of the PR + Author string + + // The SHA of the PR head commit + CommitSHA string + + // The SHA of the PR base commit (the branch we're merging into) + BaseSHA string + + // Files is a list of files that were changed in the PR + Files []string +} + +func (p *PullRequest) Link() string { + return fmt.Sprintf("https://github.com/kubernetes/enhancements/pull/%d", p.Number) +} diff --git a/experimental/keptain/pkg/store/kep.go b/experimental/keptain/pkg/store/kep.go new file mode 100644 index 0000000..efaaac5 --- /dev/null +++ b/experimental/keptain/pkg/store/kep.go @@ -0,0 +1,177 @@ +package store + +import ( + "context" + "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 + pullRequests []*model.PullRequest +} + +// NewRepository creates a new KEP repository instance +func NewRepository(ctx context.Context, 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) + } + if err := r.indexPullRequests(ctx); err != nil { + return nil, fmt.Errorf("error indexing pull requests: %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 +} diff --git a/experimental/keptain/pkg/store/prs.go b/experimental/keptain/pkg/store/prs.go new file mode 100644 index 0000000..0666a33 --- /dev/null +++ b/experimental/keptain/pkg/store/prs.go @@ -0,0 +1,123 @@ +package store + +import ( + "context" + "fmt" + "maps" + "slices" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/google/go-github/v69/github" + "k8s.io/klog/v2" + "sigs.k8s.io/maintainers/experiments/keptain/pkg/model" +) + +func (r *Repository) indexPullRequests(ctx context.Context) error { + githubClient := github.NewClient(nil) + githubPullRequests, _, err := githubClient.PullRequests.List(ctx, "kubernetes", "enhancements", nil) + if err != nil { + return fmt.Errorf("error listing pull requests: %w", err) + } + + var prs []*model.PullRequest + + for _, githubPullRequest := range githubPullRequests { + prs = append(prs, &model.PullRequest{ + Number: githubPullRequest.GetNumber(), + Title: githubPullRequest.GetTitle(), + Author: githubPullRequest.GetUser().GetLogin(), + CommitSHA: githubPullRequest.GetHead().GetSHA(), + BaseSHA: githubPullRequest.GetBase().GetSHA(), + }) + } + + // For each PR, we need a list of commits, and changes files. + // This lets us associate a PR with a KEP. + gitRepo, err := gogit.PlainOpen(r.basePath) + if err != nil { + return fmt.Errorf("error opening git repository: %w", err) + } + + for _, pr := range prs { + klog.Infof("indexing PR %d", pr.Number) + + headCommit, err := gitRepo.CommitObject(plumbing.NewHash(pr.CommitSHA)) + if err != nil { + return fmt.Errorf("error getting head commit %q for PR %d: %w", pr.CommitSHA, pr.Number, err) + } + + baseCommit, err := gitRepo.CommitObject(plumbing.NewHash(pr.BaseSHA)) + if err != nil { + return fmt.Errorf("error getting base commit %q for PR %d: %w", pr.BaseSHA, pr.Number, err) + } + + mergeBase, err := headCommit.MergeBase(baseCommit) + if err != nil { + return fmt.Errorf("error getting merge base for PR %d: %w", pr.Number, err) + } + if len(mergeBase) == 0 { + return fmt.Errorf("no merge base found for PR %d", pr.Number) + } + if len(mergeBase) > 1 { + return fmt.Errorf("multiple merge bases found for PR %d", pr.Number) + } + + commitIter, err := gitRepo.Log(&gogit.LogOptions{ + From: plumbing.NewHash(pr.CommitSHA), + }) + if err != nil { + return fmt.Errorf("reading commits in PR %d from %s: %w", pr.Number, pr.CommitSHA, err) + } + + var commits []*object.Commit + stopAt := mergeBase[0].Hash + for { + commit, err := commitIter.Next() + if err != nil { + klog.Infof("mergeBase is %v", stopAt) + return fmt.Errorf("walking commit log for PR %d from %s: %w", pr.Number, pr.CommitSHA, err) + } + if commit.Hash == stopAt { + break + } + commits = append(commits, commit) + } + + baseTree, err := commits[len(commits)-1].Tree() + if err != nil { + return fmt.Errorf("error getting base tree for PR %d: %w", pr.Number, err) + } + headTree, err := commits[0].Tree() + if err != nil { + return fmt.Errorf("error getting head tree for PR %d: %w", pr.Number, err) + } + + changes, err := baseTree.Diff(headTree) + if err != nil { + return fmt.Errorf("error getting diff for PR %d: %w", pr.Number, err) + } + files := make(map[string]bool) + for _, change := range changes { + if change.To.Name != "" { + files[change.To.Name] = true + } + if change.From.Name != "" { + files[change.From.Name] = true + } + } + + pr.Files = slices.Sorted(maps.Keys(files)) + + // TODO: Get the KEP path from the PR files. + } + r.pullRequests = prs + + return nil +} + +// ListPullRequests returns a list of all pull requests in the repository. +func (r *Repository) ListPullRequests() ([]*model.PullRequest, error) { + return r.pullRequests, nil +} diff --git a/experimental/keptain/pkg/website/server.go b/experimental/keptain/pkg/website/server.go new file mode 100644 index 0000000..5c6062c --- /dev/null +++ b/experimental/keptain/pkg/website/server.go @@ -0,0 +1,192 @@ +package website + +import ( + "bytes" + "fmt" + "html/template" + "net/http" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "k8s.io/klog/v2" + "sigs.k8s.io/maintainers/experiments/keptain/pkg/model" + "sigs.k8s.io/maintainers/experiments/keptain/pkg/store" +) + +// Server is the main HTTP server for the website +type Server struct { + kepRepo *store.Repository +} + +// NewServer creates a new Server +func NewServer(kepRepo *store.Repository) *Server { + return &Server{kepRepo: kepRepo} +} + +// Run starts the server, and listens on the given endpoint forever. +func (s *Server) Run(endpoint string) error { + mux := http.NewServeMux() + + // Serve static files + fs := http.FileServer(http.Dir("static")) + mux.Handle("GET /static/", http.StripPrefix("/static/", fs)) + + // Routes + mux.HandleFunc("GET /", s.handleHome) + mux.HandleFunc("GET /api/search", s.handleSearch) + mux.HandleFunc("GET /kep/{path...}", s.handleKEP) + mux.HandleFunc("GET /pullrequests/", s.handlePullRequestList) + + fmt.Println("Server starting on :8080...") + return http.ListenAndServe(endpoint, mux) +} + +// HomePageData is the data model for the home page +type HomePageData struct { + AllWorkflows []*model.KEP + Query string +} + +// handleHome handles the home page, which is the list of all KEPs +func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := klog.FromContext(ctx) + + query := r.URL.Query().Get("q") + log.Info("Listing all workflows", "query", query) + + keps, err := s.kepRepo.ListKEPs(query) + if err != nil { + http.Error(w, fmt.Sprintf("Error loading KEPs: %v", err), http.StatusInternalServerError) + return + } + + data := HomePageData{ + AllWorkflows: keps, + Query: query, + } + + tmpl := template.Must(template.ParseFiles("templates/home.html")) + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + } +} + +// handleSearch handles the search fragment on the homepage, used when typing into the list. +// We may be able to harmonize this with handleHome in future. +func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := klog.FromContext(ctx) + + query := r.URL.Query().Get("q") + log.Info("Searching KEPs", "query", query) + + keps, err := s.kepRepo.ListKEPs(query) + if err != nil { + http.Error(w, fmt.Sprintf("Error loading KEPs: %v", err), http.StatusInternalServerError) + return + } + + data := HomePageData{ + AllWorkflows: keps, + Query: query, + } + + tmpl := template.Must(template.ParseFiles("templates/home.html")) + if err := tmpl.ExecuteTemplate(w, "kep_list", data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + } +} + +// KEPPageData is the data model for the KEP "detail" page +type KEPPageData struct { + Workflow *model.KEP + ContentHTML template.HTML +} + +// handleKEP handles the KEP "detail" page, which is the page that displays the KEP content. +func (s *Server) handleKEP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := klog.FromContext(ctx) + + path := r.PathValue("path") + + log.Info("Listing KEP", "path", path) + + kep, err := s.kepRepo.GetKEP(path) + if err != nil { + http.Error(w, fmt.Sprintf("Error loading KEP: %v", err), http.StatusNotFound) + return + } + + // Idea: Maybe we should pre-render the markdown to HTML in the store, + // and just serve the HTML here? + + // Configure markdown processor with GitHub Flavored Markdown + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, + extension.Typographer, + extension.Table, + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + html.WithUnsafe(), // Required for GFM tables and task lists + html.WithXHTML(), + ), + ) + + // Create a new parser context with TOC enabled + context := parser.NewContext() + + // Parse the markdown content + var buf bytes.Buffer + doc := md.Parser().Parse(text.NewReader([]byte(kep.TextContents)), parser.WithContext(context)) + if err := md.Renderer().Render(&buf, []byte(kep.TextContents), doc); err != nil { + http.Error(w, fmt.Sprintf("Error converting markdown: %v", err), http.StatusInternalServerError) + return + } + + data := KEPPageData{ + Workflow: kep, + ContentHTML: template.HTML(buf.String()), + } + + tmpl := template.Must(template.ParseFiles("templates/kep.html")) + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + } +} + +// PullRequestsListPageData is the data model for the PR "list" page +type PullRequestsListPageData struct { + PullRequests []*model.PullRequest +} + +// handlePullRequestList handles the PR "list" page, which is the page that displays the list of pull requests against KEPs. +func (s *Server) handlePullRequestList(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := klog.FromContext(ctx) + + log.Info("Listing KEP pull requests") + + prs, err := s.kepRepo.ListPullRequests() + if err != nil { + http.Error(w, fmt.Sprintf("error listing pull requests: %v", err), http.StatusInternalServerError) + return + } + + data := PullRequestsListPageData{ + PullRequests: prs, + } + + tmpl := template.Must(template.ParseFiles("templates/pullrequests/list.html")) + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + } +} diff --git a/experimental/keptain/static/css/style.css b/experimental/keptain/static/css/style.css new file mode 100644 index 0000000..8942a47 --- /dev/null +++ b/experimental/keptain/static/css/style.css @@ -0,0 +1,377 @@ +:root { + --primary-color: #326ce5; + --background-color: #f5f7fa; + --text-color: #2c3e50; + --card-background: #ffffff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--background-color); +} + +header { + background-color: var(--primary-color); + color: white; + padding: 1rem 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +header h1 { + margin: 0; +} + +header nav { + margin-top: 1rem; +} + +.nav-tabs { + display: flex; + gap: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding-bottom: 0.5rem; +} + +.nav-tabs .tab { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + padding: 0.5rem 1rem; + border-radius: 4px; + transition: all 0.2s ease; +} + +.nav-tabs .tab:hover { + color: white; + background-color: rgba(255, 255, 255, 0.1); +} + +.nav-tabs .tab.active { + color: white; + background-color: rgba(255, 255, 255, 0.15); + font-weight: 500; +} + +header nav a { + color: white; + text-decoration: none; +} + +main { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +.kep-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.kep-card { + background-color: var(--card-background); + border-radius: 8px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: transform 0.2s ease; +} + +.kep-card:hover { + transform: translateY(-2px); +} + +.kep-card h2 { + margin-bottom: 1rem; + color: var(--primary-color); +} + +.kep-card a { + color: inherit; + text-decoration: none; +} + +.kep-detail { + background-color: var(--background-color); + border-radius: 8px; + padding: 0.5rem; +} + +.kep-detail h1 { + color: var(--primary-color); + margin-bottom: 1.5rem; +} + +.kep-metadata { + background-color: var(--card-background); + border-radius: 8px; + padding: 1.5rem; + color: #666; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + margin-bottom: 2rem; +} + +.kep-metadata p { + margin: 0.5rem 0; +} + +.kep-metadata strong { + color: #24292e; +} + +.kep-content { + background-color: var(--card-background); + border-radius: 8px; + padding: 1.25rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem 1rem; + margin-top: 1rem; +} + +dt { + font-weight: bold; +} + +footer { + text-align: center; + padding: 2rem; + color: #666; + border-top: 1px solid #ddd; +} + +.search-container { + max-width: 800px; + margin: 20px auto; + padding: 0 20px; + position: relative; +} + +.search-container input[type="text"] { + width: 100%; + padding: 12px 20px; + font-size: 16px; + border: 2px solid #ddd; + border-radius: 6px; + transition: border-color 0.2s ease; +} + +.search-container input[type="text"]:focus { + outline: none; + border-color: var(--primary-color); +} + +.search-status { + margin-top: 8px; + color: #666; + font-size: 14px; + text-align: right; +} + +.search-container button { + padding: 10px 20px; + background-color: #326ce5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +.search-container button:hover { + background-color: #2857b8; +} + +.htmx-indicator { + display: none; + position: absolute; + right: 30px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border: 3px solid #f3f3f3; + border-top: 3px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: translateY(-50%) rotate(0deg); } + 100% { transform: translateY(-50%) rotate(360deg); } +} + +.htmx-request .htmx-indicator { + display: block; +} + +.htmx-request.htmx-indicator { + display: block; +} + +/* + * Try to style our markdown content to render similar to how Github renders it. + * (Because this is the renderer we've used when creating most of the KEPs) +*/ +.markdown-body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; + color: #24292e; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1 { + font-size: 2em; + padding-bottom: 0.3em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + font-size: 1.5em; + padding-bottom: 0.3em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h3 { + font-size: 1.25em; +} + +.markdown-body h4 { + font-size: 1em; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 16px; + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + word-wrap: break-all; +} + +.markdown-body li + li { + margin-top: 0.25em; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; +} + +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; + margin-bottom: 16px; +} + +.markdown-body pre code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; + margin: 0 0 16px 0; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; + margin-top: 0; + margin-bottom: 16px; + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/experimental/keptain/templates/home.html b/experimental/keptain/templates/home.html new file mode 100644 index 0000000..0b2e8f6 --- /dev/null +++ b/experimental/keptain/templates/home.html @@ -0,0 +1,52 @@ + + + + + + Kubernetes KEP Explorer + + + + +
+

Kubernetes KEP Explorer

+ +
+
+
+ +
+
+
+
+ {{template "kep_list" .}} +
+
+ + + + +{{define "kep_list"}} + {{range .AllWorkflows}} +
+

{{.Title}}

+
+

KEP Number: {{.Number}}

+

Status: {{.Status}}

+

Authors: {{range .Authors}}{{.}} {{end}}

+
+
+ {{end}} +{{end}} \ No newline at end of file diff --git a/experimental/keptain/templates/kep.html b/experimental/keptain/templates/kep.html new file mode 100644 index 0000000..b9b632b --- /dev/null +++ b/experimental/keptain/templates/kep.html @@ -0,0 +1,35 @@ + + + + + + {{.Workflow.Title}} - Kubernetes KEP Explorer + + + +
+

Kubernetes KEP Explorer

+ +
+
+
+

{{.Workflow.Title}}

+ + +
+ {{.ContentHTML}} +
+
+
+ + + \ No newline at end of file diff --git a/experimental/keptain/templates/pullrequests/list.html b/experimental/keptain/templates/pullrequests/list.html new file mode 100644 index 0000000..aae3f04 --- /dev/null +++ b/experimental/keptain/templates/pullrequests/list.html @@ -0,0 +1,36 @@ + + + + + + Kubernetes KEP Explorer + + + + +
+

Kubernetes KEP Explorer

+ +
+
+
+ {{range .PullRequests}} +
+

{{.Title}}

+ +
+ {{end}} +
+ + + +