github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/template/parse.go (about) 1 package template 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/hashicorp/go-multierror" 16 "github.com/mitchellh/mapstructure" 17 ) 18 19 // rawTemplate is the direct JSON document format of the template file. 20 // This is what is decoded directly from the file, and then it is turned 21 // into a Template object thereafter. 22 type rawTemplate struct { 23 MinVersion string `mapstructure:"min_packer_version"` 24 Description string 25 26 Builders []map[string]interface{} 27 Push map[string]interface{} 28 PostProcessors []interface{} `mapstructure:"post-processors"` 29 Provisioners []map[string]interface{} 30 Variables map[string]interface{} 31 32 RawContents []byte 33 } 34 35 // Template returns the actual Template object built from this raw 36 // structure. 37 func (r *rawTemplate) Template() (*Template, error) { 38 var result Template 39 var errs error 40 41 // Copy some literals 42 result.Description = r.Description 43 result.MinVersion = r.MinVersion 44 result.RawContents = r.RawContents 45 46 // Gather the variables 47 if len(r.Variables) > 0 { 48 result.Variables = make(map[string]*Variable, len(r.Variables)) 49 } 50 for k, rawV := range r.Variables { 51 var v Variable 52 53 // Variable is required if the value is exactly nil 54 v.Required = rawV == nil 55 56 // Weak decode the default if we have one 57 if err := r.decoder(&v.Default, nil).Decode(rawV); err != nil { 58 errs = multierror.Append(errs, fmt.Errorf( 59 "variable %s: %s", k, err)) 60 continue 61 } 62 63 result.Variables[k] = &v 64 } 65 66 // Let's start by gathering all the builders 67 if len(r.Builders) > 0 { 68 result.Builders = make(map[string]*Builder, len(r.Builders)) 69 } 70 for i, rawB := range r.Builders { 71 var b Builder 72 if err := mapstructure.WeakDecode(rawB, &b); err != nil { 73 errs = multierror.Append(errs, fmt.Errorf( 74 "builder %d: %s", i+1, err)) 75 continue 76 } 77 78 // Set the raw configuration and delete any special keys 79 b.Config = rawB 80 delete(b.Config, "name") 81 delete(b.Config, "type") 82 if len(b.Config) == 0 { 83 b.Config = nil 84 } 85 86 // If there is no type set, it is an error 87 if b.Type == "" { 88 errs = multierror.Append(errs, fmt.Errorf( 89 "builder %d: missing 'type'", i+1)) 90 continue 91 } 92 93 // The name defaults to the type if it isn't set 94 if b.Name == "" { 95 b.Name = b.Type 96 } 97 98 // If this builder already exists, it is an error 99 if _, ok := result.Builders[b.Name]; ok { 100 errs = multierror.Append(errs, fmt.Errorf( 101 "builder %d: builder with name '%s' already exists", 102 i+1, b.Name)) 103 continue 104 } 105 106 // Append the builders 107 result.Builders[b.Name] = &b 108 } 109 110 // Gather all the post-processors 111 if len(r.PostProcessors) > 0 { 112 result.PostProcessors = make([][]*PostProcessor, 0, len(r.PostProcessors)) 113 } 114 for i, v := range r.PostProcessors { 115 // Parse the configurations. We need to do this because post-processors 116 // can take three different formats. 117 configs, err := r.parsePostProcessor(i, v) 118 if err != nil { 119 errs = multierror.Append(errs, err) 120 continue 121 } 122 123 // Parse the PostProcessors out of the configs 124 pps := make([]*PostProcessor, 0, len(configs)) 125 for j, c := range configs { 126 var pp PostProcessor 127 if err := r.decoder(&pp, nil).Decode(c); err != nil { 128 errs = multierror.Append(errs, fmt.Errorf( 129 "post-processor %d.%d: %s", i+1, j+1, err)) 130 continue 131 } 132 133 // Type is required 134 if pp.Type == "" { 135 errs = multierror.Append(errs, fmt.Errorf( 136 "post-processor %d.%d: type is required", i+1, j+1)) 137 continue 138 } 139 140 // Set the configuration 141 delete(c, "except") 142 delete(c, "only") 143 delete(c, "keep_input_artifact") 144 delete(c, "type") 145 if len(c) > 0 { 146 pp.Config = c 147 } 148 149 pps = append(pps, &pp) 150 } 151 152 result.PostProcessors = append(result.PostProcessors, pps) 153 } 154 155 // Gather all the provisioners 156 if len(r.Provisioners) > 0 { 157 result.Provisioners = make([]*Provisioner, 0, len(r.Provisioners)) 158 } 159 for i, v := range r.Provisioners { 160 var p Provisioner 161 if err := r.decoder(&p, nil).Decode(v); err != nil { 162 errs = multierror.Append(errs, fmt.Errorf( 163 "provisioner %d: %s", i+1, err)) 164 continue 165 } 166 167 // Type is required before any richer validation 168 if p.Type == "" { 169 errs = multierror.Append(errs, fmt.Errorf( 170 "provisioner %d: missing 'type'", i+1)) 171 continue 172 } 173 174 // Copy the configuration 175 delete(v, "except") 176 delete(v, "only") 177 delete(v, "override") 178 delete(v, "pause_before") 179 delete(v, "type") 180 if len(v) > 0 { 181 p.Config = v 182 } 183 184 // TODO: stuff 185 result.Provisioners = append(result.Provisioners, &p) 186 } 187 188 // Push 189 if len(r.Push) > 0 { 190 var p Push 191 if err := r.decoder(&p, nil).Decode(r.Push); err != nil { 192 errs = multierror.Append(errs, fmt.Errorf( 193 "push: %s", err)) 194 } 195 196 result.Push = p 197 } 198 199 // If we have errors, return those with a nil result 200 if errs != nil { 201 return nil, errs 202 } 203 204 return &result, nil 205 } 206 207 func (r *rawTemplate) decoder( 208 result interface{}, 209 md *mapstructure.Metadata) *mapstructure.Decoder { 210 d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 211 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 212 Metadata: md, 213 Result: result, 214 }) 215 if err != nil { 216 // This really shouldn't happen since we have firm control over 217 // all the arguments and they're all unit tested. So we use a 218 // panic here to note this would definitely be a bug. 219 panic(err) 220 } 221 return d 222 } 223 224 func (r *rawTemplate) parsePostProcessor( 225 i int, raw interface{}) ([]map[string]interface{}, error) { 226 switch v := raw.(type) { 227 case string: 228 return []map[string]interface{}{ 229 {"type": v}, 230 }, nil 231 case map[string]interface{}: 232 return []map[string]interface{}{v}, nil 233 case []interface{}: 234 var err error 235 result := make([]map[string]interface{}, len(v)) 236 for j, innerRaw := range v { 237 switch innerV := innerRaw.(type) { 238 case string: 239 result[j] = map[string]interface{}{"type": innerV} 240 case map[string]interface{}: 241 result[j] = innerV 242 case []interface{}: 243 err = multierror.Append(err, fmt.Errorf( 244 "post-processor %d.%d: sequence not allowed to be nested in a sequence", 245 i+1, j+1)) 246 default: 247 err = multierror.Append(err, fmt.Errorf( 248 "post-processor %d.%d: unknown format", 249 i+1, j+1)) 250 } 251 } 252 253 if err != nil { 254 return nil, err 255 } 256 257 return result, nil 258 default: 259 return nil, fmt.Errorf("post-processor %d: bad format", i+1) 260 } 261 } 262 263 // Parse takes the given io.Reader and parses a Template object out of it. 264 func Parse(r io.Reader) (*Template, error) { 265 // Create a buffer to copy what we read 266 var buf bytes.Buffer 267 if _, err := buf.ReadFrom(r); err != nil { 268 return nil, err 269 } 270 271 // First, decode the object into an interface{}. We do this instead of 272 // the rawTemplate directly because we'd rather use mapstructure to 273 // decode since it has richer errors. 274 var raw interface{} 275 if err := json.Unmarshal(buf.Bytes(), &raw); err != nil { 276 return nil, err 277 } 278 279 // Create our decoder 280 var md mapstructure.Metadata 281 var rawTpl rawTemplate 282 rawTpl.RawContents = buf.Bytes() 283 decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 284 Metadata: &md, 285 Result: &rawTpl, 286 }) 287 if err != nil { 288 return nil, err 289 } 290 291 // Do the actual decode into our structure 292 if err := decoder.Decode(raw); err != nil { 293 return nil, err 294 } 295 296 // Build an error if there are unused root level keys 297 if len(md.Unused) > 0 { 298 sort.Strings(md.Unused) 299 for _, unused := range md.Unused { 300 // Ignore keys starting with '_' as comments 301 if unused[0] == '_' { 302 continue 303 } 304 305 err = multierror.Append(err, fmt.Errorf( 306 "Unknown root level key in template: '%s'", unused)) 307 } 308 } 309 if err != nil { 310 return nil, err 311 } 312 313 // Return the template parsed from the raw structure 314 return rawTpl.Template() 315 } 316 317 // ParseFile is the same as Parse but is a helper to automatically open 318 // a file for parsing. 319 func ParseFile(path string) (*Template, error) { 320 var f *os.File 321 var err error 322 if path == "-" { 323 // Create a temp file for stdin in case of errors 324 f, err = ioutil.TempFile(os.TempDir(), "packer") 325 if err != nil { 326 return nil, err 327 } 328 defer os.Remove(f.Name()) 329 defer f.Close() 330 io.Copy(f, os.Stdin) 331 f.Seek(0, os.SEEK_SET) 332 } else { 333 f, err = os.Open(path) 334 if err != nil { 335 return nil, err 336 } 337 defer f.Close() 338 } 339 tpl, err := Parse(f) 340 if err != nil { 341 syntaxErr, ok := err.(*json.SyntaxError) 342 if !ok { 343 return nil, err 344 } 345 // Rewind the file and get a better error 346 f.Seek(0, os.SEEK_SET) 347 // Grab the error location, and return a string to point to offending syntax error 348 line, col, highlight := highlightPosition(f, syntaxErr.Offset) 349 err = fmt.Errorf("Error parsing JSON: %s\nAt line %d, column %d (offset %d):\n%s", err, line, col, syntaxErr.Offset, highlight) 350 return nil, err 351 } 352 353 if !filepath.IsAbs(path) { 354 path, err = filepath.Abs(path) 355 if err != nil { 356 return nil, err 357 } 358 } 359 360 tpl.Path = path 361 return tpl, nil 362 } 363 364 // Takes a file and the location in bytes of a parse error 365 // from json.SyntaxError.Offset and returns the line, column, 366 // and pretty-printed context around the error with an arrow indicating the exact 367 // position of the syntax error. 368 func highlightPosition(f *os.File, pos int64) (line, col int, highlight string) { 369 // Modified version of the function in Camlistore by Brad Fitzpatrick 370 // https://github.com/camlistore/camlistore/blob/4b5403dd5310cf6e1ae8feb8533fd59262701ebc/vendor/go4.org/errorutil/highlight.go 371 line = 1 372 // New io.Reader for file 373 br := bufio.NewReader(f) 374 // Initialize lines 375 lastLine := "" 376 thisLine := new(bytes.Buffer) 377 // Loop through template to find line, column 378 for n := int64(0); n < pos; n++ { 379 // read byte from io.Reader 380 b, err := br.ReadByte() 381 if err != nil { 382 break 383 } 384 // If end of line, save line as previous line in case next line is offender 385 if b == '\n' { 386 lastLine = thisLine.String() 387 thisLine.Reset() 388 line++ 389 col = 1 390 } else { 391 // Write current line, until line is safe, or error point is encountered 392 col++ 393 thisLine.WriteByte(b) 394 } 395 } 396 397 // Populate highlight string to place a '^' char at offending column 398 if line > 1 { 399 highlight += fmt.Sprintf("%5d: %s\n", line-1, lastLine) 400 } 401 402 highlight += fmt.Sprintf("%5d: %s\n", line, thisLine.String()) 403 highlight += fmt.Sprintf("%s^\n", strings.Repeat(" ", col+5)) 404 return 405 }