github.com/snyk/vervet/v3@v3.7.0/internal/generator/generator.go (about)

     1  package generator
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/ghodss/yaml"
    15  
    16  	"github.com/snyk/vervet/v3"
    17  	"github.com/snyk/vervet/v3/config"
    18  )
    19  
    20  // Generator generates files for new resources from data models and templates.
    21  type Generator struct {
    22  	name     string
    23  	filename *template.Template
    24  	contents *template.Template
    25  	files    *template.Template
    26  	data     map[string]*template.Template
    27  
    28  	debug bool
    29  	force bool
    30  }
    31  
    32  var (
    33  	templateFuncs = template.FuncMap{
    34  		"map": func(keyValues ...interface{}) (map[string]interface{}, error) {
    35  			if len(keyValues)%2 != 0 {
    36  				return nil, fmt.Errorf("invalid number of arguments to map")
    37  			}
    38  			m := make(map[string]interface{}, len(keyValues)/2)
    39  			for i := 0; i < len(keyValues); i += 2 {
    40  				k, ok := keyValues[i].(string)
    41  				if !ok {
    42  					return nil, fmt.Errorf("map keys must be strings")
    43  				}
    44  				m[k] = keyValues[i+1]
    45  			}
    46  			return m, nil
    47  		},
    48  		"indent": func(indent int, s string) string {
    49  			return strings.ReplaceAll(s, "\n", "\n"+strings.Repeat(" ", indent))
    50  		},
    51  		"uncapitalize": func(s string) string {
    52  			if len(s) > 1 {
    53  				return strings.ToLower(s[0:1]) + s[1:]
    54  			}
    55  			return s
    56  		},
    57  		"capitalize": func(s string) string {
    58  			if len(s) > 1 {
    59  				return strings.ToUpper(s[0:1]) + s[1:]
    60  			}
    61  			return s
    62  		},
    63  		"replaceall": strings.ReplaceAll,
    64  	}
    65  )
    66  
    67  func withIncludeFunc(t *template.Template) *template.Template {
    68  	return t.Funcs(template.FuncMap{
    69  		"include": func(name string, data interface{}) (string, error) {
    70  			buf := bytes.NewBuffer(nil)
    71  			if err := t.ExecuteTemplate(buf, name, data); err != nil {
    72  				return "", err
    73  			}
    74  			return buf.String(), nil
    75  		},
    76  	})
    77  }
    78  
    79  // NewMap instanstiates a map of all Generators defined in a
    80  // Project.
    81  func NewMap(proj *config.Project, options ...Option) (map[string]*Generator, error) {
    82  	result := map[string]*Generator{}
    83  	for name, genConf := range proj.Generators {
    84  		g, err := New(genConf, options...)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  		result[name] = g
    89  	}
    90  	return result, nil
    91  }
    92  
    93  // New returns a new Generator from config.
    94  func New(conf *config.Generator, options ...Option) (*Generator, error) {
    95  	g := &Generator{
    96  		name: conf.Name,
    97  		data: map[string]*template.Template{},
    98  	}
    99  	for i := range options {
   100  		options[i](g)
   101  	}
   102  	if g.debug {
   103  		log.Printf("generator %s: debug logging enabled", g.name)
   104  	}
   105  
   106  	contentsTemplate, err := ioutil.ReadFile(conf.Template)
   107  	if err != nil {
   108  		return nil, fmt.Errorf("%w: (generators.%s.contents)", err, conf.Name)
   109  	}
   110  	g.contents, err = template.New("contents").Funcs(templateFuncs).Parse(string(contentsTemplate))
   111  	if err != nil {
   112  		return nil, fmt.Errorf("%w: (generators.%s.contents)", err, conf.Name)
   113  	}
   114  	if conf.Filename != "" {
   115  		g.filename, err = template.New("filename").Funcs(templateFuncs).Parse(conf.Filename)
   116  		if err != nil {
   117  			return nil, fmt.Errorf("%w: (generators.%s.filename)", err, conf.Name)
   118  		}
   119  	}
   120  	if conf.Files != "" {
   121  		g.files, err = withIncludeFunc(g.contents.New("files")).Parse(conf.Files)
   122  		if err != nil {
   123  			return nil, fmt.Errorf("%w: (generators.%s.files)", err, conf.Name)
   124  		}
   125  	}
   126  	if len(conf.Data) > 0 {
   127  		for fieldName, genData := range conf.Data {
   128  			g.data[fieldName], err = template.New("include").Funcs(templateFuncs).Parse(genData.Include)
   129  			if err != nil {
   130  				return nil, fmt.Errorf("%w: (generators.%s.data.%s.include)", err, conf.Name, fieldName)
   131  			}
   132  		}
   133  	}
   134  	return g, nil
   135  }
   136  
   137  // Option configures a Generator.
   138  type Option func(g *Generator)
   139  
   140  // Force configures the Generator to overwrite generated artifacts.
   141  func Force(force bool) Option {
   142  	return func(g *Generator) {
   143  		g.force = true
   144  	}
   145  }
   146  
   147  // Debug turns on template debug logging.
   148  func Debug(debug bool) Option {
   149  	return func(g *Generator) {
   150  		g.debug = true
   151  	}
   152  }
   153  
   154  // VersionScope identifies a distinct resource version that the generator is
   155  // building for.
   156  type VersionScope struct {
   157  	API       string
   158  	Resource  string
   159  	Version   string
   160  	Stability string
   161  }
   162  
   163  func (s *VersionScope) validate() error {
   164  	_, err := vervet.ParseVersion(s.Version)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	_, err = vervet.ParseStability(s.Stability)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	return nil
   173  }
   174  
   175  type versionScope struct {
   176  	*VersionScope
   177  	Data map[string]interface{}
   178  }
   179  
   180  // Run executes the Generator. If generated artifacts already exist, a warning
   181  // is logged but the file is not overwritten, unless force is true.
   182  func (g *Generator) Run(scope *VersionScope) error {
   183  	err := scope.validate()
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	// Derive data
   189  	data := map[string]interface{}{}
   190  	for fieldName, tmpl := range g.data {
   191  		var buf bytes.Buffer
   192  		err := tmpl.ExecuteTemplate(&buf, "include", scope)
   193  		if err != nil {
   194  			return fmt.Errorf("failed to resolve filename: %w (generators.%s.data.%s.include)", err, g.name, fieldName)
   195  		}
   196  		filename := strings.TrimSpace(buf.String())
   197  		if g.debug {
   198  			log.Printf("interpolated generators.%s.data.%s.include => %q", g.name, fieldName, filename)
   199  		}
   200  		contents, err := ioutil.ReadFile(filename)
   201  		if err != nil {
   202  			return fmt.Errorf("%w (generators.%s.data.%s.include)", err, g.name, fieldName)
   203  		}
   204  		fieldValue := map[string]interface{}{}
   205  		switch filepath.Ext(filename) {
   206  		case ".yaml":
   207  			err = yaml.Unmarshal(contents, &fieldValue)
   208  			if err != nil {
   209  				return fmt.Errorf("failed to load %q: %w (generators.%s.data.%s.include)", filename, err, g.name, fieldName)
   210  			}
   211  		case ".json":
   212  			err = json.Unmarshal(contents, &fieldValue)
   213  			if err != nil {
   214  				return fmt.Errorf("failed to load %q: %w (generators.%s.data.%s.include)", filename, err, g.name, fieldName)
   215  			}
   216  		default:
   217  			return fmt.Errorf("don't know how to load %q: %w (generators.%s.data.%s.include)", filename, err, g.name, fieldName)
   218  		}
   219  		data[fieldName] = fieldValue
   220  	}
   221  	gsc := &versionScope{
   222  		VersionScope: scope,
   223  		Data:         data,
   224  	}
   225  	if g.files != nil {
   226  		return g.runFiles(gsc)
   227  	}
   228  	return g.runFile(gsc)
   229  }
   230  
   231  func (g *Generator) runFile(scope *versionScope) error {
   232  	var filenameBuf bytes.Buffer
   233  	err := g.filename.ExecuteTemplate(&filenameBuf, "filename", scope)
   234  	if err != nil {
   235  		return fmt.Errorf("failed to resolve filename: %w (generators.%s.filename)", err, g.name)
   236  	}
   237  	filename := filenameBuf.String()
   238  	if g.debug {
   239  		log.Printf("interpolated generators.%s.filename => %q", g.name, filename)
   240  	}
   241  	if _, err := os.Stat(filename); err == nil && !g.force {
   242  		log.Printf("not overwriting existing file %q", filename)
   243  		return nil
   244  	}
   245  	parentDir := filepath.Dir(filename)
   246  	err = os.MkdirAll(parentDir, 0777)
   247  	if err != nil {
   248  		return fmt.Errorf("failed to create %q: %w: (generators.%s.filename)", parentDir, err, g.name)
   249  	}
   250  	f, err := os.Create(filename)
   251  	if err != nil {
   252  		return fmt.Errorf("failed to create %q: %w: (generators.%s.filename)", filename, err, g.name)
   253  	}
   254  	defer f.Close()
   255  	err = g.contents.ExecuteTemplate(f, "contents", scope)
   256  	if err != nil {
   257  		return fmt.Errorf("template failed: %w (generators.%s.filename)", err, g.name)
   258  	}
   259  	return nil
   260  }
   261  
   262  func (g *Generator) runFiles(scope *versionScope) error {
   263  	var filesBuf bytes.Buffer
   264  	err := g.files.ExecuteTemplate(&filesBuf, "files", scope)
   265  	if err != nil {
   266  		return fmt.Errorf("%w: (generators.%s.files)", err, g.name)
   267  	}
   268  	if g.debug {
   269  		log.Printf("interpolated generators.%s.files => %q", g.name, filesBuf.String())
   270  	}
   271  	files := map[string]string{}
   272  	err = yaml.Unmarshal(filesBuf.Bytes(), &files)
   273  	if err != nil {
   274  		// TODO: dump output for debugging?
   275  		return fmt.Errorf("failed to load output as yaml: %w: (generators.%s.files)", err, g.name)
   276  	}
   277  	for filename, contents := range files {
   278  		dir := filepath.Dir(filename)
   279  		err := os.MkdirAll(dir, 0777)
   280  		if err != nil {
   281  			return fmt.Errorf("failed to create directory %q: %w (generators.%s.files)", dir, err, g.name)
   282  		}
   283  		if _, err := os.Stat(filename); err == nil && !g.force {
   284  			log.Printf("not overwriting existing file %q", filename)
   285  			continue
   286  		}
   287  		err = ioutil.WriteFile(filename, []byte(contents), 0777)
   288  		if err != nil {
   289  			return fmt.Errorf("failed to write file %q: %w (generators.%s.files)", filename, err, g.name)
   290  		}
   291  	}
   292  	return nil
   293  }