package config import ( "fmt" "reflect" "sort" "strings" "github.com/hashicorp/go-multierror" "github.com/hashicorp/packer/template/interpolate" "github.com/mitchellh/mapstructure" ) // DecodeOpts are the options for decoding configuration. type DecodeOpts struct { // Metadata, if non-nil, will be set to the metadata post-decode Metadata *mapstructure.Metadata // Interpolate, if true, will automatically interpolate the // configuration with the given InterpolateContext. User variables // will be automatically detected and added in-place to the given // context. Interpolate bool InterpolateContext *interpolate.Context InterpolateFilter *interpolate.RenderFilter } // Decode decodes the configuration into the target and optionally // automatically interpolates all the configuration as it goes. func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { if config == nil { config = &DecodeOpts{Interpolate: true} } // Interpolate first if config.Interpolate { // Detect user variables from the raws and merge them into our context ctx, err := DetectContext(raws...) if err != nil { return err } if config.InterpolateContext == nil { config.InterpolateContext = ctx } else { config.InterpolateContext.BuildName = ctx.BuildName config.InterpolateContext.BuildType = ctx.BuildType config.InterpolateContext.TemplatePath = ctx.TemplatePath config.InterpolateContext.UserVariables = ctx.UserVariables } ctx = config.InterpolateContext // Render everything for i, raw := range raws { m, err := interpolate.RenderMap(raw, ctx, config.InterpolateFilter) if err != nil { return err } raws[i] = m } } // Build our decoder var md mapstructure.Metadata decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: target, Metadata: &md, WeaklyTypedInput: true, DecodeHook: mapstructure.ComposeDecodeHookFunc( uint8ToStringHook, mapstructure.StringToSliceHookFunc(","), mapstructure.StringToTimeDurationHookFunc(), ), }) if err != nil { return err } for _, raw := range raws { if err := decoder.Decode(raw); err != nil { return err } } // Set the metadata if it is set if config.Metadata != nil { *config.Metadata = md } // If we have unused keys, it is an error if len(md.Unused) > 0 { var err error sort.Strings(md.Unused) for _, unused := range md.Unused { if unused != "type" && !strings.HasPrefix(unused, "packer_") { err = multierror.Append(err, fmt.Errorf( "unknown configuration key: %q", unused)) } } if err != nil { return err } } return nil } // DetectContext builds a base interpolate.Context, automatically // detecting things like user variables from the raw configuration params. func DetectContext(raws ...interface{}) (*interpolate.Context, error) { var s struct { BuildName string `mapstructure:"packer_build_name"` BuildType string `mapstructure:"packer_builder_type"` TemplatePath string `mapstructure:"packer_template_path"` Vars map[string]string `mapstructure:"packer_user_variables"` } for _, r := range raws { if err := mapstructure.Decode(r, &s); err != nil { return nil, err } } return &interpolate.Context{ BuildName: s.BuildName, BuildType: s.BuildType, TemplatePath: s.TemplatePath, UserVariables: s.Vars, }, nil } func uint8ToStringHook(f reflect.Kind, t reflect.Kind, v interface{}) (interface{}, error) { // We need to convert []uint8 to string. We have to do this // because internally Packer uses MsgPack for RPC and the MsgPack // codec turns strings into []uint8 if f == reflect.Slice && t == reflect.String { dataVal := reflect.ValueOf(v) dataType := dataVal.Type() elemKind := dataType.Elem().Kind() if elemKind == reflect.Uint8 { v = string(dataVal.Interface().([]uint8)) } } return v, nil }