mirror of https://github.com/coder/coder.git
parent
410a7d54ee
commit
5dd436c19b
6
Makefile
6
Makefile
|
@ -428,7 +428,7 @@ else
|
||||||
endif
|
endif
|
||||||
.PHONY: fmt/shfmt
|
.PHONY: fmt/shfmt
|
||||||
|
|
||||||
lint: lint/shellcheck lint/go lint/ts lint/helm lint/site-icons
|
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
|
|
||||||
lint/site-icons:
|
lint/site-icons:
|
||||||
|
@ -447,6 +447,10 @@ lint/go:
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
.PHONY: lint/go
|
.PHONY: lint/go
|
||||||
|
|
||||||
|
lint/examples:
|
||||||
|
go run ./scripts/examplegen/main.go -lint
|
||||||
|
.PHONY: lint/examples
|
||||||
|
|
||||||
# Use shfmt to determine the shell files, takes editorconfig into consideration.
|
# Use shfmt to determine the shell files, takes editorconfig into consideration.
|
||||||
lint/shellcheck: $(SHELL_SRC_FILES)
|
lint/shellcheck: $(SHELL_SRC_FILES)
|
||||||
echo "--- shellcheck"
|
echo "--- shellcheck"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
display_name: Incus System Container with Docker
|
display_name: Incus System Container with Docker
|
||||||
description: Develop in an Incus System Container with Docker using incus
|
description: Develop in an Incus System Container with Docker using incus
|
||||||
icon: /icon/lxc.svg
|
icon: ../../../site/static/icon/lxc.svg
|
||||||
maintainer_github: coder
|
maintainer_github: coder
|
||||||
verified: true
|
verified: true
|
||||||
tags: [local, incus, lxc, lxd]
|
tags: [local, incus, lxc, lxd]
|
||||||
|
|
|
@ -3,9 +3,12 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -24,124 +27,198 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
lint := flag.Bool("lint", false, "Lint **all** the examples instead of generating the examples.gen.json file")
|
||||||
panic(err)
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := run(*lint); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "error: %+v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
//nolint:revive // This is a script, not a library.
|
||||||
|
func run(lint bool) error {
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
src, err := parser.ParseFile(fset, filepath.Join(examplesDir, examplesSrc), nil, parser.ParseComments)
|
src, err := parser.ParseFile(fset, filepath.Join(examplesDir, examplesSrc), nil, parser.ParseComments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectFS := os.DirFS(".")
|
||||||
|
examplesFS := os.DirFS(examplesDir)
|
||||||
|
|
||||||
var paths []string
|
var paths []string
|
||||||
for _, comment := range src.Comments {
|
if lint {
|
||||||
for _, line := range comment.List {
|
files, err := fs.ReadDir(examplesFS, "templates")
|
||||||
if s, ok := parseEmbedTag(line.Text); ok && !strings.HasSuffix(s, ".json") {
|
if err != nil {
|
||||||
paths = append(paths, s)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paths = append(paths, filepath.Join("templates", f.Name()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, comment := range src.Comments {
|
||||||
|
for _, line := range comment.List {
|
||||||
|
if s, ok := parseEmbedTag(line.Text); ok && !strings.HasSuffix(s, ".json") {
|
||||||
|
paths = append(paths, s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var examples []codersdk.TemplateExample
|
var examples []codersdk.TemplateExample
|
||||||
files := os.DirFS(examplesDir)
|
var errs []error
|
||||||
for _, name := range paths {
|
for _, name := range paths {
|
||||||
dir, err := fs.Stat(files, name)
|
te, err := parseTemplateExample(projectFS, examplesFS, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
errs = append(errs, err)
|
||||||
}
|
|
||||||
if !dir.IsDir() {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
exampleID := dir.Name()
|
if te != nil {
|
||||||
// Each one of these is a example!
|
examples = append(examples, *te)
|
||||||
readme, err := fs.ReadFile(files, path.Join(name, "README.md"))
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("example %q does not contain README.md", exampleID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frontMatter, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(readme))
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("parse example %q front matter: %w", exampleID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nameRaw, exists := frontMatter.FrontMatter["display_name"]
|
|
||||||
if !exists {
|
|
||||||
return xerrors.Errorf("example %q front matter does not contain name", exampleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
name, valid := nameRaw.(string)
|
|
||||||
if !valid {
|
|
||||||
return xerrors.Errorf("example %q name isn't a string", exampleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptionRaw, exists := frontMatter.FrontMatter["description"]
|
|
||||||
if !exists {
|
|
||||||
return xerrors.Errorf("example %q front matter does not contain name", exampleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
description, valid := descriptionRaw.(string)
|
|
||||||
if !valid {
|
|
||||||
return xerrors.Errorf("example %q description isn't a string", exampleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := []string{}
|
|
||||||
tagsRaw, exists := frontMatter.FrontMatter["tags"]
|
|
||||||
if exists {
|
|
||||||
tagsI, valid := tagsRaw.([]interface{})
|
|
||||||
if !valid {
|
|
||||||
return xerrors.Errorf("example %q tags isn't a slice: type %T", exampleID, tagsRaw)
|
|
||||||
}
|
|
||||||
for _, tagI := range tagsI {
|
|
||||||
tag, valid := tagI.(string)
|
|
||||||
if !valid {
|
|
||||||
return xerrors.Errorf("example %q tag isn't a string: type %T", exampleID, tagI)
|
|
||||||
}
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var icon string
|
|
||||||
iconRaw, exists := frontMatter.FrontMatter["icon"]
|
|
||||||
if exists {
|
|
||||||
icon, valid = iconRaw.(string)
|
|
||||||
if !valid {
|
|
||||||
return xerrors.Errorf("example %q icon isn't a string", exampleID)
|
|
||||||
}
|
|
||||||
icon, err = filepath.Rel("../site/static/", filepath.Join(examplesDir, name, icon))
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("example %q icon is not in site/static: %w", exampleID, err)
|
|
||||||
}
|
|
||||||
// The FE needs a static path!
|
|
||||||
icon = "/" + icon
|
|
||||||
}
|
|
||||||
|
|
||||||
examples = append(examples, codersdk.TemplateExample{
|
|
||||||
ID: exampleID,
|
|
||||||
Name: name,
|
|
||||||
Description: description,
|
|
||||||
Icon: icon,
|
|
||||||
Tags: tags,
|
|
||||||
Markdown: string(frontMatter.Content),
|
|
||||||
|
|
||||||
// URL is set by examples/examples.go.
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w := os.Stdout
|
if len(errs) > 0 {
|
||||||
|
return xerrors.Errorf("parse failed: %w", errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
var w io.Writer = os.Stdout
|
||||||
|
if lint {
|
||||||
|
w = io.Discard
|
||||||
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprint(w, "// Code generated by examplegen. DO NOT EDIT.\n")
|
_, err = fmt.Fprint(w, "// Code generated by examplegen. DO NOT EDIT.\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
return enc.Encode(examples)
|
return enc.Encode(examples)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTemplateExample(projectFS, examplesFS fs.FS, name string) (te *codersdk.TemplateExample, err error) {
|
||||||
|
var errs []error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
errs = append([]error{err}, errs...)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = xerrors.Errorf("example %q has errors", name)
|
||||||
|
for _, e := range errs {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dir, err := fs.Stat(examplesFS, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !dir.IsDir() {
|
||||||
|
//nolint:nilnil // This is a script, not a library.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleID := dir.Name()
|
||||||
|
// Each one of these is a example!
|
||||||
|
readme, err := fs.ReadFile(examplesFS, path.Join(name, "README.md"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.New("missing README.md")
|
||||||
|
}
|
||||||
|
|
||||||
|
frontMatter, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(readme))
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("parse front matter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure validation here is in sync with requirements for
|
||||||
|
// coder/registry.
|
||||||
|
displayName, err := getString(frontMatter.FrontMatter, "display_name")
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
description, err := getString(frontMatter.FrontMatter, "description")
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = getString(frontMatter.FrontMatter, "maintainer_github")
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := []string{}
|
||||||
|
tagsRaw, exists := frontMatter.FrontMatter["tags"]
|
||||||
|
if exists {
|
||||||
|
tagsI, valid := tagsRaw.([]interface{})
|
||||||
|
if !valid {
|
||||||
|
errs = append(errs, xerrors.Errorf("tags isn't a slice: type %T", tagsRaw))
|
||||||
|
} else {
|
||||||
|
for _, tagI := range tagsI {
|
||||||
|
tag, valid := tagI.(string)
|
||||||
|
if !valid {
|
||||||
|
errs = append(errs, xerrors.Errorf("tag isn't a string: type %T", tagI))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon string
|
||||||
|
icon, err = getString(frontMatter.FrontMatter, "icon")
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
cleanPath := filepath.Clean(filepath.Join(examplesDir, name, icon))
|
||||||
|
_, err := fs.Stat(projectFS, cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, xerrors.Errorf("icon does not exist: %w", err))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(cleanPath, filepath.Join("site", "static")) {
|
||||||
|
errs = append(errs, xerrors.Errorf("icon is not in site/static/: %q", icon))
|
||||||
|
}
|
||||||
|
icon, err = filepath.Rel(filepath.Join("site", "static"), cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, xerrors.Errorf("cannot make icon relative to site/static: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, xerrors.New("front matter validation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &codersdk.TemplateExample{
|
||||||
|
ID: exampleID,
|
||||||
|
Name: displayName,
|
||||||
|
Description: description,
|
||||||
|
Icon: "/" + icon, // The FE needs a static path!
|
||||||
|
Tags: tags,
|
||||||
|
Markdown: string(frontMatter.Content),
|
||||||
|
|
||||||
|
// URL is set by examples/examples.go.
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getString(m map[string]any, key string) (string, error) {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok {
|
||||||
|
return "", xerrors.Errorf("front matter does not contain %q", key)
|
||||||
|
}
|
||||||
|
vv, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", xerrors.Errorf("%q isn't a string", key)
|
||||||
|
}
|
||||||
|
return vv, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseEmbedTag(s string) (string, bool) {
|
func parseEmbedTag(s string) (string, bool) {
|
||||||
if !strings.HasPrefix(s, "//go:embed") {
|
if !strings.HasPrefix(s, "//go:embed") {
|
||||||
return "", false
|
return "", false
|
||||||
|
|
Loading…
Reference in New Issue