pkg/osutil/osutil.go (180 lines of code) (raw):

package osutil import ( "bytes" "fmt" "io/fs" "os" "path" "regexp" "runtime" "strings" "syscall" "text/template" log "github.com/sirupsen/logrus" "github.com/Azure/draft/pkg/config" "github.com/Azure/draft/pkg/templatewriter" ) // A draft variable is defined as a string of non-whitespace characters wrapped in double curly braces. var draftVariableRegex = regexp.MustCompile("{{[^\\s.]+\\S*}}") const configFileName = "draft.yaml" // Exists returns whether the given file or directory exists or not. func Exists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return true, err } // SymlinkWithFallback attempts to symlink a file or directory, but falls back to a move operation // in the event of the user not having the required privileges to create the symlink. func SymlinkWithFallback(oldname, newname string) (err error) { err = os.Symlink(oldname, newname) if runtime.GOOS == "windows" { // If creating the symlink fails on Windows because the user // does not have the required privileges, ignore the error and // fall back to renaming the file. // // ERROR_PRIVILEGE_NOT_HELD is 0x522: // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx if lerr, ok := err.(*os.LinkError); ok && lerr.Err == syscall.Errno(0x522) { err = os.Rename(oldname, newname) } } return } // EnsureDirectory checks if a directory exists and creates it if it doesn't func EnsureDirectory(dir string) error { if fi, err := os.Stat(dir); err != nil { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("could not create %s: %s", dir, err) } } else if !fi.IsDir() { return fmt.Errorf("%s must be a directory", dir) } return nil } // EnsureFile checks if a file exists and creates it if it doesn't func EnsureFile(file string) error { fi, err := os.Stat(file) if err != nil { f, err := os.Create(file) if err != nil { return fmt.Errorf("could not create %s: %s", file, err) } defer f.Close() } else if fi.IsDir() { return fmt.Errorf("%s must not be a directory", file) } return nil } func CopyDir( fileSys fs.FS, src, dest string, draftConfig *config.DraftConfig, templateWriter templatewriter.TemplateWriter) error { files, err := fs.ReadDir(fileSys, src) if err != nil { return err } for _, f := range files { if f.Name() == configFileName { continue } fileName := f.Name() if overrideName, ok := draftConfig.FileNameOverrideMap[f.Name()]; ok { fileName = overrideName } srcPath := path.Join(src, f.Name()) destPath := path.Join(dest, fileName) log.Debugf("Source path: %s Dest path: %s", srcPath, destPath) if f.IsDir() { if err = templateWriter.EnsureDirectory(destPath); err != nil { return err } if err = CopyDir(fileSys, srcPath, destPath, draftConfig, templateWriter); err != nil { return err } } else { fileContent, err := replaceTemplateVariables(fileSys, srcPath, draftConfig) if err != nil { return err } if err = checkAllVariablesSubstituted(string(fileContent)); err != nil { return fmt.Errorf("error substituting file %s: %w", srcPath, err) } if err = templateWriter.WriteFile(destPath, fileContent); err != nil { return err } } } return nil } /* checkAllVariablesSubstituted checks that all draft variables have been substituted. If any draft variables are found, an error is returned. Draft variables are defined as a string of non-whitespace characters starting with a non-period character wrapped in double curly braces. The non-period first character constraint is used to avoid matching helm template functions. */ func checkAllVariablesSubstituted(fileContent string) error { if unsubstitutedVars := draftVariableRegex.FindAllString(fileContent, -1); len(unsubstitutedVars) > 0 { unsubstitutedVarsString := strings.Join(unsubstitutedVars, ", ") return fmt.Errorf("unsubstituted variable: %s", unsubstitutedVarsString) } return nil } func replaceTemplateVariables(fileSys fs.FS, srcPath string, draftConfig *config.DraftConfig) ([]byte, error) { file, err := fs.ReadFile(fileSys, srcPath) if err != nil { return nil, err } fileString := string(file) for _, variable := range draftConfig.Variables { log.Debugf("replacing %s with %s", variable.Name, variable.Value) fileString = strings.ReplaceAll(fileString, "{{"+variable.Name+"}}", variable.Value) } return []byte(fileString), nil } // CopyDirWithTemplates - Handles Gotemplate processing and writing func CopyDirWithTemplates( fileSys fs.FS, src, dest string, draftConfig *config.DraftConfig, templateWriter templatewriter.TemplateWriter) error { files, err := fs.ReadDir(fileSys, src) if err != nil { return err } for _, f := range files { if f.Name() == configFileName { continue } fileName := f.Name() if overrideName, ok := draftConfig.FileNameOverrideMap[f.Name()]; ok { fileName = overrideName } srcPath := path.Join(src, f.Name()) destPath := path.Join(dest, fileName) log.Debugf("Source path: %s Dest path: %s", srcPath, destPath) variableMap := draftConfig.GetVariableMap() if len(variableMap) == 0 { return fmt.Errorf("variable map is empty, unable to replace template variables") } if f.IsDir() { if err = templateWriter.EnsureDirectory(destPath); err != nil { return err } if err = CopyDirWithTemplates(fileSys, srcPath, destPath, draftConfig, templateWriter); err != nil { return err } } else { fileContent, err := replaceGoTemplateVariables(fileSys, srcPath, variableMap) if err != nil { return err } if err = templateWriter.WriteFile(destPath, fileContent); err != nil { return err } } } return nil } func replaceGoTemplateVariables(fileSys fs.FS, srcPath string, variableMap map[string]string) ([]byte, error) { file, err := fs.ReadFile(fileSys, srcPath) if err != nil { return nil, err } // Parse the template file, missingkey=error ensures an error will be returned if any variable is missing during template execution. tmpl, err := template.New("template").Option("missingkey=error").Parse(string(file)) if err != nil { return nil, err } // Execute the template with variableMap var buf bytes.Buffer err = tmpl.Execute(&buf, variableMap) if err != nil { return nil, err } return buf.Bytes(), nil }