diff --git a/scripts/generate-static-site.sh b/scripts/generate-static-site.sh new file mode 100755 index 0000000000..efec9c299a --- /dev/null +++ b/scripts/generate-static-site.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Copyright 2023 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "${REPO_ROOT}"/tools/generate-static-site + +go build -o "${REPO_ROOT}/bin/generate-static-site" . + +cd "${REPO_ROOT}" +rm -rf websites/kpt.dev +"${REPO_ROOT}/bin/generate-static-site" +cp -r site/static/ websites/kpt.dev/static/ \ No newline at end of file diff --git a/site/gitops/README.md b/site/gitops/README.md index bc92119090..b440d880ff 100644 --- a/site/gitops/README.md +++ b/site/gitops/README.md @@ -5,4 +5,4 @@ deployment mechanism is [Config Sync](gitops/configsync/), but since configurati repositories, other [GitOps](https://opengitops.dev/) tools can also be used. Currently supported gitops tools: -* [Config Sync](gitops/configsync/) \ No newline at end of file +* [Config Sync](configsync/) \ No newline at end of file diff --git a/tools/generate-static-site/go.mod b/tools/generate-static-site/go.mod new file mode 100644 index 0000000000..f09e596764 --- /dev/null +++ b/tools/generate-static-site/go.mod @@ -0,0 +1,18 @@ +module github.com/GoogleContainerTools/kpt/tools/generate-static-site + +go 1.20 + +require ( + github.com/igorsobreira/titlecase v0.0.0-20140109233139-4156b5b858ac + github.com/yuin/goldmark v1.4.6 + github.com/yuin/goldmark-meta v1.1.0 + golang.org/x/net v0.8.0 + k8s.io/klog/v2 v2.90.1 +) + +require ( + github.com/go-logr/logr v1.2.3 // indirect + github.com/kr/text v0.2.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/tools/generate-static-site/go.sum b/tools/generate-static-site/go.sum new file mode 100644 index 0000000000..97d408b722 --- /dev/null +++ b/tools/generate-static-site/go.sum @@ -0,0 +1,25 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/igorsobreira/titlecase v0.0.0-20140109233139-4156b5b858ac h1:AfRcPFr4uK97K6YaYi9XmNY/cTPF+cLspaXocdqkdCQ= +github.com/igorsobreira/titlecase v0.0.0-20140109233139-4156b5b858ac/go.mod h1:KOzUkqpWM2xArNm82cehGc5PBFYV1Qadzzt81aJi7F0= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/yuin/goldmark v1.4.6 h1:EQ1OkiNq/eMbQxs/2O/A8VDIHERXGH14s19ednd4XIw= +github.com/yuin/goldmark v1.4.6/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= diff --git a/tools/generate-static-site/main.go b/tools/generate-static-site/main.go new file mode 100644 index 0000000000..0984562bac --- /dev/null +++ b/tools/generate-static-site/main.go @@ -0,0 +1,583 @@ +// Copyright 2023 The kpt Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "context" + "embed" + "flag" + "fmt" + "html/template" + "io/fs" + "os" + "path" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + + "golang.org/x/net/html" + + "github.com/igorsobreira/titlecase" + "github.com/yuin/goldmark" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/ast" + gast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + mdhtml "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + "k8s.io/klog/v2" +) + +//go:embed templates/* +var templates embed.FS + +const markdownExtension = ".md" +const introPage = "00.md" + +var pagePrefix = regexp.MustCompile(`^\d\d-?`) + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + srcBase := "site/" + out := "websites/kpt.dev/" + + flag.Parse() + + md := goldmark.New( + goldmark.WithExtensions( + meta.Meta, // ignore yaml metadata + extension.Table, // render tables + &normalizeLinks{}, // .md -> .html + ), + + goldmark.WithRendererOptions( + mdhtml.WithUnsafe(), + ), + ) + + // Build the sidebar + sidebarMarkdown := "" + { + templatePath := "templates/sidebar_template.md.tmpl" + + t := template.Must( + template.New(path.Base(templatePath)). + Funcs(template.FuncMap{"bookLayout": getBookOutline}). + ParseFS(templates, templatePath)) + + var w bytes.Buffer + fmt.Fprintf(&w, "") + err := t.Execute(&w, nil) + if err != nil { + return fmt.Errorf("running template: %w", err) + } + sidebarMarkdown = w.String() + } + sidebar := "" + { + var w bytes.Buffer + if err := md.Convert([]byte(sidebarMarkdown), &w); err != nil { + return fmt.Errorf("rendering markdown: %w", err) + } + sidebar = w.String() + } + + // Go through and build pages for each markdown file + var pages []*Page + if err := filepath.WalkDir(srcBase, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + ext := filepath.Ext(path) + + b, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("reading file %q: %w", path, err) + } + src := string(b) + + srcPath := strings.TrimPrefix(path, srcBase) + + var page *Page + + ext = strings.ToLower(ext) + switch ext { + case ".html": + page = &Page{ + URL: "/" + srcPath, + HTML: src, + } + + case ".md": + + // Remove the hugo hide directives that we are using + { + re := regexp.MustCompile("(?ms)" + regexp.QuoteMeta("{{% hide %}}") + ".+?" + regexp.QuoteMeta("{{% /hide %}}")) + src = re.ReplaceAllLiteralString(src, "") + re2 := regexp.MustCompile("{{.*}}") + src = re2.ReplaceAllLiteralString(src, "") + } + + var htmlString string + { + var w bytes.Buffer + mdContext := parser.NewContext() + if err := md.Convert([]byte(src), &w, parser.WithContext(mdContext)); err != nil { + return fmt.Errorf("rendering markdown: %w", err) + } + htmlString = w.String() + + metaData := meta.Get(mdContext) + + title := "" + if v, ok := metaData["title"]; ok { + title = v.(string) + } + + if strings.HasPrefix(srcPath, "book/") { + title = getBookPageTitle(srcPath) + if title == "" { + klog.Warningf("cannot parse book page title for %q", path) + } + } + if title != "" { + var w2 bytes.Buffer + if err := md.Convert([]byte(title), &w2); err != nil { + return fmt.Errorf("rendering markdown: %w", err) + } + + htmlString = "

" + w2.String() + "

" + htmlString + } + } + + isCover := srcPath == "coverpage.md" + if isCover { + templateName := "templates/cover.html" + template, err := templates.ReadFile(templateName) + if err != nil { + return fmt.Errorf("reading template %q: %w", templateName, err) + } + + htmlString = strings.ReplaceAll(string(template), "", htmlString) + } + + { + templateName := "templates/main.html" + template, err := templates.ReadFile(templateName) + if err != nil { + return fmt.Errorf("reading template %q: %w", templateName, err) + } + + htmlString = strings.ReplaceAll(string(template), "", htmlString) + } + { + templateName := "templates/index.html" + template, err := templates.ReadFile(templateName) + if err != nil { + return fmt.Errorf("reading template %q: %w", templateName, err) + } + + htmlString = strings.ReplaceAll(string(template), "", htmlString) + } + + if strings.Contains(htmlString, "") { + htmlString = strings.ReplaceAll(htmlString, "", sidebar) + } + + page = &Page{ + URL: "/" + srcPath, + HTML: htmlString, + } + + default: + klog.V(2).Infof("ignoring file with unknown extension %q", srcPath) + } + + if page != nil { + page.URL = strings.TrimSuffix(page.URL, "/README.md") + page.URL = strings.TrimSuffix(page.URL, "/00.md") + page.URL = strings.TrimSuffix(page.URL, ".md") + + pages = append(pages, page) + } + + return nil + }); err != nil { + return err + } + + for _, page := range pages { + v := &PageVisitor{ + Page: page, + } + if err := v.postProcessHTML(); err != nil { + return err + } + } + + for _, page := range pages { + outURL := page.URL + + if !strings.HasSuffix(outURL, ".html") { + outURL = outURL + ".html" + } + + outPath := filepath.Join(out, strings.TrimPrefix(outURL, "/")) + klog.V(2).Infof("writing file %v", outPath) + + outDir := filepath.Dir(outPath) + if err := os.MkdirAll(outDir, 0755); err != nil { + return fmt.Errorf("making directories %q: %w", outDir, err) + } + + if err := os.WriteFile(outPath, []byte(page.HTML), 0644); err != nil { + return fmt.Errorf("writing file %q: %w", outPath, err) + } + } + + return nil +} + +func getBookOutline() string { + sourcePath := "site/book" + chapters := collectChapters(sourcePath) + + return getChapterBlock(chapters) +} + +func collectChapters(source string) []chapter { + chapters := make([]chapter, 0) + chapterDirs, err := os.ReadDir(source) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + for _, dir := range chapterDirs { + if dir.IsDir() { + chapters = append(chapters, getChapter(dir.Name(), filepath.Join(source, dir.Name()))) + } + } + + return chapters +} + +func getChapter(chapterDirName string, chapterDirPath string) chapter { + chapterBuilder := chapter{} + + // Split into chapter number and hyphenated name + splitDirName := strings.SplitN(chapterDirName, "-", 2) + chapterBuilder.Number = splitDirName[0] + chapterBuilder.Name = titlecase.Title(strings.ReplaceAll(splitDirName[1], "-", " ")) + + pageFiles, err := os.ReadDir(chapterDirPath) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + for _, pageFile := range pageFiles { + if filepath.Ext(pageFile.Name()) == markdownExtension && pagePrefix.MatchString(pageFile.Name()) { + chapterBuilder.Pages = append(chapterBuilder.Pages, + getPage(pageFile.Name(), chapterBuilder.Name, chapterDirPath)) + } + } + + return chapterBuilder +} + +func getPage(pageFileName string, defaultName string, parentPath string) page { + // Split into page number and hyphenated name. + splitPageName := strings.SplitN(pageFileName, "-", 2) + + pageName := defaultName + if pageFileName != introPage { + // Strip page number and extension from file name. + pageTitle := pagePrefix.ReplaceAll([]byte(pageFileName), []byte("")) + pageName = titlecase.Title(strings.ReplaceAll(strings.ReplaceAll(string(pageTitle), ".md", ""), "-", " ")) + } + + p := page{ + Number: splitPageName[0], + Name: pageName, + Path: filepath.Join(parentPath, pageFileName), + } + + if pageFileName == introPage { + p.Path = parentPath + } + return p +} + +func getChapterBlock(chapters []chapter) string { + // Sort chapters in ascending order by chapter number. + sort.Slice(chapters, func(i, j int) bool { return chapters[i].Number < chapters[j].Number }) + var sb strings.Builder + for chapterIndex, chapterEntry := range chapters { + chapterNumber := chapterIndex + 1 + for pageIndex, pageEntry := range chapterEntry.Pages { + // Make path relative to site directory. + path := strings.Replace(pageEntry.Path, "site/", "/", 1) + + // Print non-chapter intro pages as children of chapter intro page. + if pageIndex == 0 { + sb.WriteString(fmt.Sprintf("\t- [%d %s](%s)\n", chapterNumber, pageEntry.Name, path)) + } else { + sb.WriteString(fmt.Sprintf("\t\t- [%d.%d %s](%s)\n", chapterNumber, pageIndex, pageEntry.Name, path)) + } + } + } + return strings.TrimRight(sb.String(), "\n") +} + +type chapter struct { + Name string + Pages []page + Number string +} + +type page struct { + Name string + Path string + Number string +} + +type normalizeLinks struct { +} + +func (x *normalizeLinks) Extend(md goldmark.Markdown) { + md.Parser().AddOptions(parser.WithASTTransformers( + util.Prioritized(&normalizeLinksTransformer{}, 999), + )) +} + +type normalizeLinksTransformer struct { +} + +func (a *normalizeLinksTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { + a.visit(node, reader.Source()) +} + +func (a *normalizeLinksTransformer) visit(node gast.Node, src []byte) { + switch node := node.(type) { + case *ast.Link: + dest := string(node.Destination) + if strings.HasSuffix(dest, ".md") { + dest = strings.TrimSuffix(dest, ".md") + } + node.Destination = []byte(dest) + default: + klog.V(2).Infof("node type %T not implmented", node) + } + + pos := node.FirstChild() + for pos != nil { + a.visit(pos, src) + pos = pos.NextSibling() + } +} + +type PageVisitor struct { + Page *Page +} + +func (v *PageVisitor) postProcessHTML() error { + doc, err := html.Parse(bytes.NewReader([]byte(v.Page.HTML))) + if err != nil { + return fmt.Errorf("parsing html: %w", err) + } + + var f func(*html.Node) + f = func(n *html.Node) { + if hasClassName(n, "sidebar-nav") { + v.addActiveSidebar(n) + } + + if hasClassName(n, "sidebar-nav") { + addSidebarCollapsibility(n) + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + + var w bytes.Buffer + if err := html.Render(&w, doc); err != nil { + return fmt.Errorf("rendering html: %w", err) + } + v.Page.HTML = w.String() + return nil +} + +// Only show child pages for currently active page to avoid sidebar cluttering. +func (v *PageVisitor) addActiveSidebar(n *html.Node) { + for _, li := range getElementsByTagName(n, "li") { + for _, a := range getElementsByTagName(li, "a") { + href := getAttr(a, "href") + href = strings.TrimSuffix(href, "/") + if href == v.Page.URL { + addClass(li, "active") + } + } + } +} + +// Only show child pages for currently active page to avoid sidebar cluttering. +func addSidebarCollapsibility(n *html.Node) { + uls := getElementsByTagName(n, "ul") + + for _, ul := range uls { + if hasClassName(ul.Parent, "active") { + continue + } + + if len(getElementsByClassName(ul, "active")) == 0 { + addClass(ul, "inactive") + } + } +} + +func getElementsByTagName(el *html.Node, tagName string) []*html.Node { + var ret []*html.Node + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == tagName { + ret = append(ret, n) + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(el) + + return ret +} + +func getElementsByClassName(el *html.Node, className string) []*html.Node { + var ret []*html.Node + var f func(*html.Node) + f = func(n *html.Node) { + if hasClassName(n, className) { + ret = append(ret, n) + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(el) + + return ret +} + +func hasClassName(el *html.Node, className string) bool { + for _, attr := range el.Attr { + if attr.Key == "class" { + tokens := strings.Split(attr.Val, " ") + for _, token := range tokens { + if token == className { + return true + } + } + } + } + return false +} + +func getAttr(el *html.Node, attrKey string) string { + for _, attr := range el.Attr { + if attr.Key == attrKey { + return attr.Val + } + } + return "" +} + +func addClass(el *html.Node, className string) { + for _, attr := range el.Attr { + if attr.Key == "class" { + tokens := strings.Split(attr.Val, " ") + for _, token := range tokens { + if token == className { + return + } + } + tokens = append(tokens, className) + attr.Val = strings.Join(tokens, " ") + return + } + } + el.Attr = append(el.Attr, html.Attribute{ + Key: "class", + Val: className, + }) +} + +type Site struct { + Pages map[string]*Page +} + +type Page struct { + URL string + + HTML string +} + +func getBookPageTitle(path string) string { + path = strings.TrimPrefix(path, "book/") + path = strings.TrimSuffix(path, ".md") + path = strings.TrimSuffix(path, "/00") + + var heading string + + components := strings.Split(path, "/") + for i, component := range components { + tokens := strings.SplitN(component, "-", 2) + if len(tokens) != 2 { + return "" + } + + n, err := strconv.Atoi(tokens[0]) + if err != nil { + return "" + } + if heading != "" { + heading += "." + } + heading = heading + fmt.Sprintf("%v", n) + if i == len(components)-1 { + return heading + " " + titlecase.Title(strings.ReplaceAll(tokens[1], "-", " ")) + } + } + + return "" +} diff --git a/tools/generate-static-site/templates/cover.html b/tools/generate-static-site/templates/cover.html new file mode 100644 index 0000000000..6d7eb0cc67 --- /dev/null +++ b/tools/generate-static-site/templates/cover.html @@ -0,0 +1,5 @@ +
+
+
+
\ No newline at end of file diff --git a/tools/generate-static-site/templates/index.html b/tools/generate-static-site/templates/index.html new file mode 100644 index 0000000000..6dca49c0c2 --- /dev/null +++ b/tools/generate-static-site/templates/index.html @@ -0,0 +1,45 @@ + + + + + + kpt - Home + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/generate-static-site/templates/main.html b/tools/generate-static-site/templates/main.html new file mode 100644 index 0000000000..7a5810adc4 --- /dev/null +++ b/tools/generate-static-site/templates/main.html @@ -0,0 +1,45 @@ +
+ + + +
+
+
+
\ No newline at end of file diff --git a/tools/generate-static-site/templates/sidebar_template.md.tmpl b/tools/generate-static-site/templates/sidebar_template.md.tmpl new file mode 100644 index 0000000000..770a3b738a --- /dev/null +++ b/tools/generate-static-site/templates/sidebar_template.md.tmpl @@ -0,0 +1,76 @@ + +- [Installation](/installation/) +- [Book](/book/) +{{bookLayout}} +- [Reference](/reference/) + - [Annotations](/reference/annotations/) + - [apply-time mutation](/reference/annotations/apply-time-mutation/) + - [depends-on](/reference/annotations/depends-on/) + - [local-config](/reference/annotations/local-config/) + - [CLI](/reference/cli/) + - [pkg](/reference/cli/pkg/) + - [diff](/reference/cli/pkg/diff/) + - [get](/reference/cli/pkg/get/) + - [init](/reference/cli/pkg/init/) + - [tree](/reference/cli/pkg/tree/) + - [update](/reference/cli/pkg/update/) + - [fn](/reference/cli/fn/) + - [render](/reference/cli/fn/render/) + - [eval](/reference/cli/fn/eval/) + - [sink](/reference/cli/fn/sink/) + - [source](/reference/cli/fn/source/) + - [live](/reference/cli/live/) + - [apply](/reference/cli/live/apply/) + - [destroy](/reference/cli/live/destroy/) + - [init](/reference/cli/live/init/) + - [install-resource-group](/reference/cli/live/install-resource-group/) + - [migrate](/reference/cli/live/migrate/) + - [status](/reference/cli/live/status/) + - [alpha](/reference/cli/alpha/) + - [license](/reference/cli/alpha/license/) + - [info](/reference/cli/alpha/license/info/) + - [live](/reference/cli/alpha/live/) + - [plan](/reference/cli/alpha/live/plan/) + - [repo](/reference/cli/alpha/repo/) + - [get](/reference/cli/alpha/repo/get/) + - [reg](/reference/cli/alpha/repo/reg/) + - [unreg](/reference/cli/alpha/repo/unreg/) + - [rpkg](/reference/cli/alpha/rpkg/) + - [get](/reference/cli/alpha/rpkg/get/) + - [pull](/reference/cli/alpha/rpkg/pull/) + - [push](/reference/cli/alpha/rpkg/push/) + - [clone](/reference/cli/alpha/rpkg/clone/) + - [init](/reference/cli/alpha/rpkg/init/) + - [propose](/reference/cli/alpha/rpkg/propose/) + - [approve](/reference/cli/alpha/rpkg/approve/) + - [del](/reference/cli/alpha/rpkg/del/) + - [propose-delete](/reference/cli/alpha/rpkg/propose-delete/) + - [reject](/reference/cli/alpha/rpkg/reject/) + - [copy](/reference/cli/alpha/rpkg/copy/) + - [sync](/reference/cli/alpha/sync/) + - [create](/reference/cli/alpha/sync/create/) + - [delete](/reference/cli/alpha/sync/delete/) + - [get](/reference/cli/alpha/sync/get/) + - [Schema](/reference/schema/) + - [Kptfile](/reference/schema/kptfile/) + - [FunctionResultList](/reference/schema/function-result-list/) + - [ResourceList](/reference/schema/resource-list/) + - [CRD Status Convention](/reference/schema/crd-status-convention/) + - [Config Connector Status Convention](/reference/schema/config-connector-status-convention/) + - [Plan](/reference/schema/plan/) +- [Functions Catalog](https://catalog.kpt.dev/ ":target=_self") + - [Curated](https://catalog.kpt.dev/ ":target=_self") + - [Contrib](https://catalog.kpt.dev/contrib/ ":target=_self") +- [GitOps](/gitops/) + - [Config Sync](/gitops/configsync/) +- [Guides](/guides/) + - [The Rationale Behind kpt](/guides/rationale.md) + - [Porch Installation Guide](/guides/porch-installation.md) + - [Porch UI Installation Guide](/guides/porch-ui-installation.md) + - [Porch User Guide](/guides/porch-user-guide.md) + - [Namespace Provisioning CLI](/guides/namespace-provisioning-cli.md) + - [Namespace Provisioning UI](/guides/namespace-provisioning-ui.md) + - [Variant Constructor Pattern](/guides/variant-constructor-pattern.md) + - [Value Propagation Pattern](/guides/value-propagation.md) +- [FAQ](/faq/) +- [Contact](/contact/) diff --git a/websites/cmd/siteserver-kpt-dev/kodata/static b/websites/cmd/siteserver-kpt-dev/kodata/static new file mode 120000 index 0000000000..067c76f6cb --- /dev/null +++ b/websites/cmd/siteserver-kpt-dev/kodata/static @@ -0,0 +1 @@ +../../../kpt.dev/ \ No newline at end of file diff --git a/websites/cmd/siteserver-kpt-dev/main.go b/websites/cmd/siteserver-kpt-dev/main.go new file mode 100644 index 0000000000..e5ffc16662 --- /dev/null +++ b/websites/cmd/siteserver-kpt-dev/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + + "k8s.io/klog/v2" +) + +func main() { + ctx := context.Background() + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + listen := ":8080" + staticRoot := "./kpt.dev" + if s := os.Getenv("KO_DATA_PATH"); s != "" { + staticRoot = filepath.Join(s, "static") + } + flag.Parse() + + httpRoot := http.Dir(staticRoot) + httpFileServer := http.FileServer(httpRoot) + + allFiles := make(map[string]fs.DirEntry) + if err := fs.WalkDir(os.DirFS(staticRoot), ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + path = "/" + path + allFiles[path] = d + klog.Infof("path %v", path) + return nil + }); err != nil { + return err + } + + rewrites := make(map[string]string) + for k, d := range allFiles { + if d.IsDir() { + if allFiles[k+".html"] != nil { + withoutSlash := strings.TrimSuffix(k, "/") + rewrites[withoutSlash] = k + ".html" + rewrites[withoutSlash+"/"] = k + ".html" + } + } else if strings.HasSuffix(k, ".html") { + rewrites[strings.TrimSuffix(k, ".html")] = k + rewrites[strings.TrimSuffix(k, ".html")+"/"] = k + } + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + upath := r.URL.Path + if !strings.HasPrefix(upath, "/") { + upath = "/" + upath + r.URL.Path = upath + } + + rewrite, ok := rewrites[r.URL.Path] + if ok { + r.URL.Path = rewrite + } + // f, err := httpRoot.Open(upath) + // if err != nil { + // if os.IsNotExist(err) { + // if _, err := httpRoot.Open(upath + ".html"); err == nil { + // r.URL.Path += ".html" + // } + // } + // } else { + // d, err := f.Stat() + // if err == nil { + // if d.IsDir() { + // if _, err := httpRoot.Open(upath + "index.html"); err == nil { + // // http server does this by default + // } else if _, err := httpRoot.Open(upath + ".html"); err == nil { + // r.URL.Path += ".html" + // } else if _, err := httpRoot.Open(upath + "README.html"); err == nil { + // r.URL.Path += "README.html" + // } else { + // s := strings.TrimSuffix(upath, "/") + // if _, err := httpRoot.Open(s + ".html"); err == nil { + // r.URL.Path += ".html" + // } + // } + // } + // } + httpFileServer.ServeHTTP(w, r) + }) + + klog.Infof("serving %q on %v", staticRoot, listen) + err := http.ListenAndServe(listen, nil) + if err != nil { + return fmt.Errorf("error serving on %q: %w", listen, err) + } + return nil +}