diff --git a/core/fs.go b/core/fs.go new file mode 100644 index 0000000..67ce91f --- /dev/null +++ b/core/fs.go @@ -0,0 +1,12 @@ +package wharf + +import "os" + +// IsDirectory checks if the given path is a directory +func isDirectory(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + return false, err + } + return info.IsDir(), nil +} diff --git a/core/function_factory.go b/core/function_factory.go new file mode 100644 index 0000000..c96aea0 --- /dev/null +++ b/core/function_factory.go @@ -0,0 +1,82 @@ +package wharf + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + + types "github.com/Makepad-fr/wharf/types" +) + +// newFunctionFactory creates a new function factory witht the given context path +func newFunctionFactory(contextPath string) functionFactory { + return functionFactory{ + contextPath: contextPath, + } +} + +// functionFactory crates a context aware functionMap for each template. +type functionFactory struct { + contextPath string // THe path of the current template file that executes this function map +} + +// Include includdes returns the content of as string. The [path] should either be +// a Dockerfile or a Dockerfile.template. If it's a Dockefile.template the [values] will +// be used to render the template using wharf. If it's a Dockerfile the cotnent will be rendered as it's. +// If any erorr happens it will returned as an error string as template function can not return errors as Go objects. +// FIXME: Users may use other filenames other then Dockerfile and Dockerifle.template especially when they are using from the same folder +func (f functionFactory) Include(path string, values types.ValueMap) string { + // Update the passed path with the context path + path = filepath.Join(f.contextPath, path) + isDir, err := isDirectory(path) + + if err != nil { + return fmt.Sprintf("Error while importing %s: %s", path, err.Error()) + } + // Create a generic error message template + const errorMessage = "Error: Include should be called either with a Dockerfile.template path or with a Dockerfile path" + if isDir { + return errorMessage + } else { + baseFileName := filepath.Base(path) + if baseFileName == "Dockerfile" { + // If the path is a Dockerfile path + file, err := os.Open(path) + if err != nil { + return fmt.Sprintf("Error: error while opening Dockerfile from %s", path) + } + content, err := io.ReadAll(file) + if err != nil { + return fmt.Sprintf("Error: error while reading file content %s", err.Error()) + } + return string(content) + } + if baseFileName == "Dockerfile.template" { + // IF the path is a Dockerfile.template + contextPath := filepath.Dir(path) + // Don't use include in included templates as it creates a cyclic dependency + // TODO: Maybe we can copy the funcMap before using it? + template, err := getTemplate(contextPath, baseFileName, map[string]any{}) + if err != nil { + return fmt.Sprintf("Error: error while getting template from %s: %s", path, err.Error()) + } + var renderedContent bytes.Buffer + err = render(template, values, &renderedContent) + if err != nil { + return fmt.Sprintf("Error: error while rendering template %s: %s", path, err.Error()) + } + return renderedContent.String() + } + return errorMessage + } +} + +// Creates the types.FuncMap for the related function factory +// FIXME: Check if it's possible to create this function map programmatically +func (f functionFactory) FuncMap() types.FuncMap { + return map[string]any{ + "include": f.Include, + } +} diff --git a/core/render.go b/core/render.go index aa972c5..9fe161e 100644 --- a/core/render.go +++ b/core/render.go @@ -13,7 +13,9 @@ import ( // Render renders the template file in the given contextPath using the values files from the given path // It writes the rendered Dockerfile to the io.Writer passed in parameters. It returns an error if something goes wrong func Render(contextPath, templateFileName, valuesFilePath string, output io.Writer) error { - template, err := getTemplate(contextPath, templateFileName) + // Create a new function map + funcMap := newFunctionFactory(contextPath).FuncMap() + template, err := getTemplate(contextPath, templateFileName, funcMap) if err != nil { return err } @@ -23,12 +25,13 @@ func Render(contextPath, templateFileName, valuesFilePath string, output io.Writ } err = render(template, values, output) - return nil + return err } // render renders the template and values to the file with the given path. // If something goes wrong it returns an error func render(tmpl *template.Template, values map[string]any, file io.Writer) error { + // Include function map to the current template just before rendering the values err := tmpl.Execute(file, values) if err != nil { return err @@ -56,21 +59,20 @@ func readValues(contextPath, valuesFilePath string) (map[string]any, error) { } // getTemplate returns the template.Template from given context path and templatefile name -func getTemplate(contextPath, templateFileName string) (*template.Template, error) { +func getTemplate(contextPath, templateFileName string, funcMap template.FuncMap) (*template.Template, error) { info, err := os.Stat(contextPath) if err != nil { return nil, err } if !info.IsDir() { - return nil, fmt.Errorf("Context path %s should be a directory path. To pass template file name use -file-name option", contextPath) + return nil, fmt.Errorf("context path %s should be a directory path. To pass template file name use -file-name option", contextPath) } contextPath = filepath.Join(contextPath, templateFileName) - templ, err := template.ParseFiles(contextPath) + templ, err := template.New(filepath.Base(templateFileName)).Funcs(funcMap).ParseFiles(contextPath) if err != nil { return nil, err } - return templ, nil } diff --git a/go.work b/go.work index 6f1fc7b..083c97e 100644 --- a/go.work +++ b/go.work @@ -1,8 +1,9 @@ -go 1.22.2 +go 1.22.4 use ( ./cli ./core ./docker-cli-plugins/commons ./docker-cli-plugins/render + ./types ) diff --git a/types/go.mod b/types/go.mod new file mode 100644 index 0000000..20a4e80 --- /dev/null +++ b/types/go.mod @@ -0,0 +1,3 @@ +module github.com/Makepad-fr/wharf/types + +go 1.22.4 diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..8e8e60c --- /dev/null +++ b/types/types.go @@ -0,0 +1,9 @@ +package types + +import "text/template" + +// TODO: template.FuncMap uses map[string]any and any is a function with an arbitrary number of arguments func(args ....), we need to check if we can create a type for that +type FuncMap = template.FuncMap + +// TODO: Change to more restrictive type as it will be used only for values +type ValueMap = map[string]any