Skip to content

refactor(examples): pre-parse frontmatter via scripts/examplegen #9514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 4, 2023
Merged
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
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ scripts/apitypings/testdata/**/*.ts
site/e2e/provisionerGenerated.ts

**/pnpm-lock.yaml

# Ignore generated JSON (e.g. examples/examples.gen.json).
**/*.gen.json
3 changes: 3 additions & 0 deletions .prettierignore.include
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ scripts/apitypings/testdata/**/*.ts
site/e2e/provisionerGenerated.ts

**/pnpm-lock.yaml

# Ignore generated JSON (e.g. examples/examples.gen.json).
**/*.gen.json
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,8 @@ gen: \
site/.prettierrc.yaml \
site/.prettierignore \
site/.eslintignore \
site/e2e/provisionerGenerated.ts
site/e2e/provisionerGenerated.ts \
examples/examples.gen.json
.PHONY: gen

# Mark all generated files as fresh so make thinks they're up-to-date. This is
Expand All @@ -494,6 +495,7 @@ gen/mark-fresh:
site/.prettierignore \
site/.eslintignore \
site/e2e/provisionerGenerated.ts \
examples/examples.gen.json \
"
for file in $$files; do
echo "$$file"
Expand Down Expand Up @@ -545,14 +547,18 @@ site/e2e/provisionerGenerated.ts:
../scripts/pnpm_install.sh
pnpm run gen:provisioner


examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(shell find ./examples/templates)
go run ./scripts/examplegen/main.go > examples/examples.gen.json

coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go

docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
go run scripts/metricsdocgen/main.go
pnpm run format:write:only ./docs/admin/prometheus.md

docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES)
docs/cli.md: scripts/clidocgen/main.go examples/examples.gen.json $(GO_SRC_FILES)
BASE_PATH="." go run ./scripts/clidocgen
pnpm run format:write:only ./docs/cli.md ./docs/cli/*.md ./docs/manifest.json

Expand Down Expand Up @@ -605,7 +611,7 @@ site/.prettierrc.yaml: .prettierrc.yaml
# - ./ -> ../
# - ./site -> ./
yq \
'.overrides[].files |= map(. | sub("^./"; "") | sub("^"; "../") | sub("../site/"; "./"))' \
'.overrides[].files |= map(. | sub("^./"; "") | sub("^"; "../") | sub("../site/"; "./") | sub("../!"; "!../"))' \
"$<" >> "$@"

# Combine .gitignore with .prettierignore.include to generate .prettierignore.
Expand Down
149 changes: 149 additions & 0 deletions examples/examples.gen.json

Large diffs are not rendered by default.

148 changes: 58 additions & 90 deletions examples/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ package examples

import (
"archive/tar"
"bufio"
"bytes"
"embed"
"encoding/json"
"io"
"io/fs"
"path"
"sort"
"strings"
"sync"

"github.com/gohugoio/hugo/parser/pageparser"
"golang.org/x/sync/singleflight"
"golang.org/x/xerrors"

Expand All @@ -19,6 +21,8 @@ import (

var (
// Only some templates are embedded that we want to display inside the UI.
// The metadata in examples.gen.json is generated via scripts/examplegen.
//go:embed examples.gen.json
//go:embed templates/aws-ecs-container
//go:embed templates/aws-linux
//go:embed templates/aws-windows
Expand All @@ -34,112 +38,76 @@ var (
files embed.FS

exampleBasePath = "https://github.com/coder/coder/tree/main/examples/templates/"
examples = make([]codersdk.TemplateExample, 0)
examplesJSON = "examples.gen.json"
parsedExamples []codersdk.TemplateExample
parseExamples sync.Once
archives = singleflight.Group{}
archives singleflight.Group
ErrNotFound = xerrors.New("example not found")
)

const rootDir = "templates"

// List returns all embedded examples.
func List() ([]codersdk.TemplateExample, error) {
var returnError error
var err error
parseExamples.Do(func() {
files, err := fs.Sub(files, rootDir)
if err != nil {
returnError = xerrors.Errorf("get example fs: %w", err)
}

dirs, err := fs.ReadDir(files, ".")
if err != nil {
returnError = xerrors.Errorf("read dir: %w", err)
return
}

for _, dir := range dirs {
if !dir.IsDir() {
continue
}
exampleID := dir.Name()
exampleURL := exampleBasePath + exampleID
// Each one of these is a example!
readme, err := fs.ReadFile(files, path.Join(dir.Name(), "README.md"))
if err != nil {
returnError = xerrors.Errorf("example %q does not contain README.md", exampleID)
return
}
parsedExamples, err = parseAndVerifyExamples()
})
return parsedExamples, err
}

frontMatter, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(readme))
if err != nil {
returnError = xerrors.Errorf("parse example %q front matter: %w", exampleID, err)
return
}
func parseAndVerifyExamples() (examples []codersdk.TemplateExample, err error) {
f, err := files.Open(examplesJSON)
if err != nil {
return nil, xerrors.Errorf("open %s: %w", examplesJSON, err)
}
defer f.Close()

nameRaw, exists := frontMatter.FrontMatter["name"]
if !exists {
returnError = xerrors.Errorf("example %q front matter does not contain name", exampleID)
return
}
b := bufio.NewReader(f)

name, valid := nameRaw.(string)
if !valid {
returnError = xerrors.Errorf("example %q name isn't a string", exampleID)
return
}
// Discard the first line (code generated by-comment).
_, _, err = b.ReadLine()
if err != nil {
return nil, xerrors.Errorf("read %s: %w", examplesJSON, err)
}

descriptionRaw, exists := frontMatter.FrontMatter["description"]
if !exists {
returnError = xerrors.Errorf("example %q front matter does not contain name", exampleID)
return
}
err = json.NewDecoder(b).Decode(&examples)
if err != nil {
return nil, xerrors.Errorf("decode %s: %w", examplesJSON, err)
}

description, valid := descriptionRaw.(string)
if !valid {
returnError = xerrors.Errorf("example %q description isn't a string", exampleID)
return
}
// Sanity-check: Verify that the examples in the JSON file match the
// embedded files.
var wantEmbedFiles []string
for i, example := range examples {
examples[i].URL = exampleBasePath + example.ID
wantEmbedFiles = append(wantEmbedFiles, example.ID)
}

tags := []string{}
tagsRaw, exists := frontMatter.FrontMatter["tags"]
if exists {
tagsI, valid := tagsRaw.([]interface{})
if !valid {
returnError = xerrors.Errorf("example %q tags isn't a slice: type %T", exampleID, tagsRaw)
return
}
for _, tagI := range tagsI {
tag, valid := tagI.(string)
if !valid {
returnError = xerrors.Errorf("example %q tag isn't a string: type %T", exampleID, tagI)
return
}
tags = append(tags, tag)
}
}
files, err := fs.Sub(files, rootDir)
if err != nil {
return nil, xerrors.Errorf("get templates fs: %w", err)
}
dirs, err := fs.ReadDir(files, ".")
if err != nil {
return nil, xerrors.Errorf("read templates dir: %w", err)
}
var gotEmbedFiles []string
for _, dir := range dirs {
if dir.IsDir() {
gotEmbedFiles = append(gotEmbedFiles, dir.Name())
}
}

var icon string
iconRaw, exists := frontMatter.FrontMatter["icon"]
if exists {
icon, valid = iconRaw.(string)
if !valid {
returnError = xerrors.Errorf("example %q icon isn't a string", exampleID)
return
}
}
sort.Strings(wantEmbedFiles)
sort.Strings(gotEmbedFiles)
want := strings.Join(wantEmbedFiles, ", ")
got := strings.Join(gotEmbedFiles, ", ")
if want != got {
return nil, xerrors.Errorf("mismatch between %s and embedded files: want %q, got %q", examplesJSON, want, got)
}

examples = append(examples, codersdk.TemplateExample{
ID: exampleID,
URL: exampleURL,
Name: name,
Description: description,
Icon: icon,
Tags: tags,
Markdown: string(frontMatter.Content),
})
}
})
return examples, returnError
return examples, nil
}

// Archive returns a tar by example ID.
Expand Down
2 changes: 1 addition & 1 deletion examples/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func TestTemplate(t *testing.T) {
t.Parallel()
list, err := examples.List()
require.NoError(t, err)
require.NoError(t, err, "error listing examples, run \"make gen\" to ensure examples are up to date")
require.NotEmpty(t, list)
for _, eg := range list {
eg := eg
Expand Down
Loading