Skip to content

Commit

Permalink
Merge pull request #79 from ulucinar/updoc
Browse files Browse the repository at this point in the history
Add updoc, the tool for publishing official provider docs
  • Loading branch information
ulucinar authored Mar 16, 2023
2 parents 35057e8 + 3a40870 commit 83bd901
Show file tree
Hide file tree
Showing 10 changed files with 924 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PLATFORMS ?= linux_amd64 linux_arm64 darwin_amd64 darwin_arm64
# Setup Go
GO_REQUIRED_VERSION = 1.19
GOLANGCILINT_VERSION ?= 1.50.0
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/uptest
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/uptest $(GO_PROJECT)/cmd/updoc
GO_LDFLAGS += -X $(GO_PROJECT)/internal/version.Version=$(VERSION)
GO_SUBDIRS += cmd internal
GO111MODULE = on
Expand Down
38 changes: 38 additions & 0 deletions cmd/updoc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Package main is the main package for updoc,
// the tool for publishing official provider docs.
package main

import (
"log"

"github.com/alecthomas/kong"
"github.com/spf13/afero"

internal "github.com/upbound/uptest/internal/updoc"
)

func main() {
opts := internal.Options{}

ctx := kong.Parse(&opts, kong.Name("updoc"),
kong.Description("Upbound enhanced document processor"),
kong.UsageOnError(),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
FlagsLast: true,
Summary: true,
}))

switch ctx.Command() {
case "generate":
if err := internal.NewIndexer(opts.Generate.DocsDir).Run(); err != nil {
log.Fatal(err)
}
case "upload":
if err := internal.New().ProcessIndex(opts.Upload, afero.NewOsFs()); err != nil {
log.Fatal(err)
}
}

// TODO(daren): garbage collection on orphaned docs after updating a version index
}
17 changes: 17 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ module github.com/upbound/uptest
go 1.19

require (
cloud.google.com/go/storage v1.27.0
github.com/adrg/frontmatter v0.2.0
github.com/alecthomas/kong v0.7.1
github.com/crossplane/crossplane-runtime v0.18.0
github.com/getkin/kin-openapi v0.108.0
github.com/google/go-cmp v0.5.9
github.com/pkg/errors v0.9.1
github.com/spf13/afero v1.8.0
github.com/tufin/oasdiff v1.2.6
golang.org/x/mod v0.7.0
google.golang.org/api v0.102.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
k8s.io/apiextensions-apiserver v0.23.0
k8s.io/apimachinery v0.23.0
Expand All @@ -17,6 +22,10 @@ require (

require (
cloud.google.com/go v0.107.0 // indirect
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
cloud.google.com/go/iam v0.6.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -25,8 +34,12 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/invopop/yaml v0.1.0 // indirect
Expand All @@ -37,13 +50,17 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/yuin/goldmark v1.5.3 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
52 changes: 52 additions & 0 deletions go.sum

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions internal/updoc/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package updoc package implements updoc,
// the tool for publishing official provider docs.
package updoc
182 changes: 182 additions & 0 deletions internal/updoc/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package updoc

import (
"encoding/json"
"fmt"
"path"
"path/filepath"
"sort"
"strings"

"github.com/adrg/frontmatter"
"github.com/spf13/afero"
)

const (
indexFN = "index.json"
sectionFN = "_index.md"
errDisplayName = "unable to find meta for %s"
)

// Sortable represents a sortable document section, like the title or a section
// of a document.
type Sortable interface {
w() int
d() string
l() string
}

// Title is the title of a document.
type Title struct {
Title string `yaml:"title"`
Weight int `yaml:"weight"`
FileLocation string
}

func (t *Title) w() int {
return t.Weight
}
func (t *Title) d() string {
return t.Title
}
func (t *Title) l() string {
return t.FileLocation
}

// Section is the section in a document.
type Section struct {
Section string `yaml:"section"`
Weight int `yaml:"weight"`
Items []Sortable
}

func (s *Section) w() int {
return s.Weight
}
func (s *Section) d() string {
return s.Section
}
func (s *Section) l() string {
return ""
}

// Item represents an item that will ultimately be represented in the uploaded
// table of contents.
type Item struct {
DisplayName string `json:"name"`
Location string `json:"location"`
}

// Indexer indexes docs.
type Indexer struct {
fs afero.Fs
root string
}

// IndexerOpt is an indexer option.
type IndexerOpt func(i *Indexer)

// WithFs sets the Indexer file system.
func WithFs(fs afero.Fs) IndexerOpt {
return func(i *Indexer) {
i.fs = fs
}
}

// NewIndexer constructs an indexer at the specified root.
func NewIndexer(root string, opts ...IndexerOpt) *Indexer {
i := &Indexer{
fs: afero.NewOsFs(),
root: filepath.Clean(root),
}
for _, o := range opts {
o(i)
}
return i
}

// Run runs the indexer.
func (i *Indexer) Run() error {
dt, err := i.processDir(i.root)
if err != nil {
return err
}

jb, err := json.MarshalIndent(flatten(*dt, "", make([]Item, 0)), "", "\t")
if err != nil {
return err
}

return afero.WriteFile(i.fs, filepath.Join(i.root, indexFN), jb, 0777)
}

func flatten(s Section, p string, a []Item) []Item {
for _, i := range s.Items {
switch v := i.(type) {
case *Section:
a = flatten(*v, path.Join(p, v.Section), a)
case *Title:
a = append(a, Item{DisplayName: path.Join(p, i.d()), Location: i.l()})
}
}
return a
}

func (i *Indexer) processDir(dir string) (*Section, error) {
fi, err := afero.ReadDir(i.fs, dir)
if err != nil {
return nil, err
}
section := Section{}
if exists, _ := afero.Exists(i.fs, path.Join(dir, sectionFN)); exists {
s, err := getMeta(i.fs, path.Join(dir, sectionFN), &section)
if err != nil {
return nil, err
}
section = *s
}
for _, e := range fi {
if e.IsDir() {
sec, err := i.processDir(path.Join(dir, e.Name()))
if err != nil {
return nil, err
}
section.Items = append(section.Items, sec)
}
if e.Name() == sectionFN || filepath.Ext(e.Name()) != ".md" {
continue
}

item, err := getMeta(i.fs, path.Join(dir, e.Name()), &Title{
// TODO(hasheddan): consider constructing directory from
// prefix when opening file rather than trimming prefix.
FileLocation: strings.TrimPrefix(path.Join(dir, e.Name()), i.root+"/"),
})
if err != nil {
return nil, err
}
section.Items = append(section.Items, item)
}
sort.SliceStable(section.Items, func(i, j int) bool {
return section.Items[i].w() < section.Items[j].w()
})
return &section, nil
}

func getMeta[T Sortable](afs afero.Fs, path string, i T) (T, error) {
// get display name, section and weight from file meta
r, err := afs.Open(path)
if err != nil {
return i, err
}

_, err = frontmatter.Parse(r, &i)
if err != nil {
return i, err
}
if i.d() == "" {
return i, fmt.Errorf(errDisplayName, path)
}

return i, nil
}
Loading

0 comments on commit 83bd901

Please sign in to comment.