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  }