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 }