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