mirror of https://github.com/knative/func.git
237 lines
5.9 KiB
Go
237 lines
5.9 KiB
Go
package function
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/boson-project/func/tarfs"
|
|
)
|
|
|
|
// Generate templates.tgz
|
|
//go:generate go run generate.go
|
|
|
|
// Embed the templates tarball
|
|
//go:embed templates.tgz
|
|
var embedded []byte
|
|
|
|
// DefaultTemplate is the default Function signature / environmental context
|
|
// of the resultant template. All runtimes are expected to have at least
|
|
// an HTTP Handler ("http") and Cloud Events ("events")
|
|
const DefaultTemplate = "http"
|
|
|
|
type templateWriter struct {
|
|
// Extensible Template Repositories
|
|
// templates on disk (extensible templates)
|
|
// Stored on disk at path:
|
|
// [customTemplatesPath]/[repository]/[runtime]/[template]
|
|
// For example
|
|
// ~/.config/func/boson/go/http"
|
|
// Specified when writing templates as simply:
|
|
// Write([runtime], [repository], [path])
|
|
// For example
|
|
// w := templateWriter{templates:"/home/username/.config/func/templates")
|
|
// w.Write("go", "boson/http")
|
|
// Ie. "Using the custom templates in the func configuration directory,
|
|
// write the Boson HTTP template for the Go runtime."
|
|
repositories string
|
|
templates fs.FS
|
|
verbose bool
|
|
}
|
|
|
|
var (
|
|
ErrRepositoryNotFound = errors.New("repository not found")
|
|
ErrRepositoriesNotDefined = errors.New("custom template location not specified")
|
|
ErrRuntimeNotFound = errors.New("runtime not found")
|
|
ErrTemplateNotFound = errors.New("template not found")
|
|
ErrTemplateMissingRepository = errors.New("template name missing repository prefix")
|
|
)
|
|
|
|
// Decompress into an in-memory FS which implements fs.ReadDirFS
|
|
func (t *templateWriter) load() (err error) {
|
|
|
|
// Templates are stored as an embedded byte slice gzip encoded.
|
|
zr, err := gzip.NewReader(bytes.NewReader(embedded))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
t.templates, err = tarfs.New(zr)
|
|
return
|
|
}
|
|
|
|
// Write the template for the given runtime to the destination specified.
|
|
// Template may be prefixed with a custom repo name.
|
|
func (t *templateWriter) Write(runtime, template, dest string) (err error) {
|
|
if t.templates == nil {
|
|
// load static embedded templates into t.templates as an fs.FS
|
|
if err = t.load(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if template == "" {
|
|
template = DefaultTemplate
|
|
}
|
|
if isCustom(template) {
|
|
return t.writeCustom(t.repositories, runtime, template, dest)
|
|
}
|
|
return t.writeEmbedded(runtime, template, dest)
|
|
}
|
|
|
|
func isCustom(template string) bool {
|
|
return len(strings.Split(template, "/")) > 1
|
|
}
|
|
|
|
func (t *templateWriter) writeCustom(repositoriesPath, runtime, template, dest string) (err error) {
|
|
if repositoriesPath == "" {
|
|
return ErrRepositoriesNotDefined
|
|
}
|
|
if !repositoryExists(repositoriesPath, template) {
|
|
return ErrRepositoryNotFound
|
|
}
|
|
cc := strings.Split(template, "/")
|
|
if len(cc) < 2 {
|
|
return ErrTemplateMissingRepository
|
|
}
|
|
repositoriesFS := os.DirFS(repositoriesPath)
|
|
|
|
runtimePath := cc[0] + "/" + runtime
|
|
_, err = fs.Stat(repositoriesFS, runtimePath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ErrRuntimeNotFound
|
|
}
|
|
|
|
templatePath := runtimePath + "/" + cc[1]
|
|
_, err = fs.Stat(repositoriesFS, templatePath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ErrTemplateNotFound
|
|
}
|
|
|
|
// ex: /home/alice/.config/func/repositories/boson/go/http
|
|
// Note that the FS instance returned by os.DirFS uses forward slashes
|
|
// internally, so source paths do not use the os path separator due to
|
|
// that breaking Windows.
|
|
src := cc[0] + "/" + runtime + "/" + cc[1]
|
|
return t.cp(src, dest, repositoriesFS)
|
|
}
|
|
|
|
func (t *templateWriter) writeEmbedded(runtime, template, dest string) error {
|
|
runtimePath := "templates/" + runtime // embedded FS alwas uses '/'
|
|
_, err := fs.Stat(t.templates, runtimePath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ErrRuntimeNotFound
|
|
}
|
|
|
|
templatePath := "templates/" + runtime + "/" + template // always '/' in embedded fs
|
|
_, err = fs.Stat(t.templates, templatePath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ErrTemplateNotFound
|
|
}
|
|
|
|
return t.cp(templatePath, dest, t.templates)
|
|
}
|
|
|
|
func repositoryExists(repositories, template string) bool {
|
|
cc := strings.Split(template, "/")
|
|
_, err := fs.Stat(os.DirFS(repositories), cc[0])
|
|
return err == nil
|
|
}
|
|
|
|
func (t *templateWriter) cp(src, dest string, files fs.FS) error {
|
|
node, err := fs.Stat(files, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if node.IsDir() {
|
|
return t.copyNode(src, dest, files)
|
|
} else {
|
|
return t.copyLeaf(src, dest, files)
|
|
}
|
|
}
|
|
|
|
func (t *templateWriter) copyNode(src, dest string, files fs.FS) error {
|
|
node, err := fs.Stat(files, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mode := node.Mode()
|
|
|
|
err = os.MkdirAll(dest, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
children, err := readDir(src, files)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, child := range children {
|
|
// NOTE: instances of fs.FS use forward slashes,
|
|
// even on Windows.
|
|
childSrc := src + "/" + child.Name()
|
|
childDest := filepath.Join(dest, child.Name())
|
|
if err = t.cp(childSrc, childDest, files); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readDir(src string, files fs.FS) ([]fs.DirEntry, error) {
|
|
f, err := files.Open(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
return nil, fmt.Errorf("%v must be a directory", fi.Name())
|
|
}
|
|
list, err := f.(fs.ReadDirFile).ReadDir(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
|
|
return list, nil
|
|
}
|
|
|
|
func (t *templateWriter) copyLeaf(src, dest string, files fs.FS) (err error) {
|
|
srcFile, err := files.Open(src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
srcFileInfo, err := fs.Stat(files, src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Use the original's mode unless a nonzero mode was explicitly provided.
|
|
mode := srcFileInfo.Mode()
|
|
|
|
destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.Copy(destFile, srcFile)
|
|
return
|
|
}
|