github.com/rothwerx/packer@v0.9.0/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 r = io.TeeReader(r, &buf) 268 269 // First, decode the object into an interface{}. We do this instead of 270 // the rawTemplate directly because we'd rather use mapstructure to 271 // decode since it has richer errors. 272 var raw interface{} 273 if err := json.NewDecoder(r).Decode(&raw); err != nil { 274 return nil, err 275 } 276 277 // Create our decoder 278 var md mapstructure.Metadata 279 var rawTpl rawTemplate 280 rawTpl.RawContents = buf.Bytes() 281 decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 282 Metadata: &md, 283 Result: &rawTpl, 284 }) 285 if err != nil { 286 return nil, err 287 } 288 289 // Do the actual decode into our structure 290 if err := decoder.Decode(raw); err != nil { 291 return nil, err 292 } 293 294 // Build an error if there are unused root level keys 295 if len(md.Unused) > 0 { 296 sort.Strings(md.Unused) 297 for _, unused := range md.Unused { 298 // Ignore keys starting with '_' as comments 299 if unused[0] == '_' { 300 continue 301 } 302 303 err = multierror.Append(err, fmt.Errorf( 304 "Unknown root level key in template: '%s'", unused)) 305 } 306 } 307 if err != nil { 308 return nil, err 309 } 310 311 // Return the template parsed from the raw structure 312 return rawTpl.Template() 313 } 314 315 // ParseFile is the same as Parse but is a helper to automatically open 316 // a file for parsing. 317 func ParseFile(path string) (*Template, error) { 318 var f *os.File 319 var err error 320 if path == "-" { 321 // Create a temp file for stdin in case of errors 322 f, err = ioutil.TempFile(os.TempDir(), "packer") 323 if err != nil { 324 return nil, err 325 } 326 defer os.Remove(f.Name()) 327 defer f.Close() 328 io.Copy(f, os.Stdin) 329 f.Seek(0, os.SEEK_SET) 330 } else { 331 f, err = os.Open(path) 332 if err != nil { 333 return nil, err 334 } 335 defer f.Close() 336 } 337 tpl, err := Parse(f) 338 if err != nil { 339 syntaxErr, ok := err.(*json.SyntaxError) 340 if !ok { 341 return nil, err 342 } 343 // Rewind the file and get a better error 344 f.Seek(0, os.SEEK_SET) 345 // Grab the error location, and return a string to point to offending syntax error 346 line, col, highlight := highlightPosition(f, syntaxErr.Offset) 347 err = fmt.Errorf("Error parsing JSON: %s\nAt line %d, column %d (offset %d):\n%s", err, line, col, syntaxErr.Offset, highlight) 348 return nil, err 349 } 350 351 if !filepath.IsAbs(path) { 352 path, err = filepath.Abs(path) 353 if err != nil { 354 return nil, err 355 } 356 } 357 358 tpl.Path = path 359 return tpl, nil 360 } 361 362 // Takes a file and the location in bytes of a parse error 363 // from json.SyntaxError.Offset and returns the line, column, 364 // and pretty-printed context around the error with an arrow indicating the exact 365 // position of the syntax error. 366 func highlightPosition(f *os.File, pos int64) (line, col int, highlight string) { 367 // Modified version of the function in Camlistore by Brad Fitzpatrick 368 // https://github.com/camlistore/camlistore/blob/4b5403dd5310cf6e1ae8feb8533fd59262701ebc/vendor/go4.org/errorutil/highlight.go 369 line = 1 370 // New io.Reader for file 371 br := bufio.NewReader(f) 372 // Initialize lines 373 lastLine := "" 374 thisLine := new(bytes.Buffer) 375 // Loop through template to find line, column 376 for n := int64(0); n < pos; n++ { 377 // read byte from io.Reader 378 b, err := br.ReadByte() 379 if err != nil { 380 break 381 } 382 // If end of line, save line as previous line in case next line is offender 383 if b == '\n' { 384 lastLine = thisLine.String() 385 thisLine.Reset() 386 line++ 387 col = 1 388 } else { 389 // Write current line, until line is safe, or error point is encountered 390 col++ 391 thisLine.WriteByte(b) 392 } 393 } 394 395 // Populate highlight string to place a '^' char at offending column 396 if line > 1 { 397 highlight += fmt.Sprintf("%5d: %s\n", line-1, lastLine) 398 } 399 400 highlight += fmt.Sprintf("%5d: %s\n", line, thisLine.String()) 401 highlight += fmt.Sprintf("%s^\n", strings.Repeat(" ", col+5)) 402 return 403 }