From 241f76b5b1440bbdac3c95b3c6b21bf594b68b6a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2015 10:44:10 -0700 Subject: [PATCH] helper/config: decoder --- helper/config/decode.go | 108 +++++++++++++++++++++++++++++++++++ helper/config/decode_test.go | 86 ++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 helper/config/decode.go create mode 100644 helper/config/decode_test.go diff --git a/helper/config/decode.go b/helper/config/decode.go new file mode 100644 index 000000000..b9dafc915 --- /dev/null +++ b/helper/config/decode.go @@ -0,0 +1,108 @@ +package config + +import ( + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/template/interpolate" +) + +// DecodeOpts are the options for decoding configuration. +type DecodeOpts struct { + // 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.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(","), + ), + }) + if err != nil { + return err + } + for _, raw := range raws { + if err := decoder.Decode(raw); 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 { + 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{ + 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 +} diff --git a/helper/config/decode_test.go b/helper/config/decode_test.go new file mode 100644 index 000000000..43aa615a7 --- /dev/null +++ b/helper/config/decode_test.go @@ -0,0 +1,86 @@ +package config + +import ( + "reflect" + "testing" + + "github.com/mitchellh/packer/template/interpolate" +) + +func TestDecode(t *testing.T) { + type Target struct { + Name string + Address string + } + + cases := map[string]struct { + Input []interface{} + Output *Target + Opts *DecodeOpts + }{ + "basic": { + []interface{}{ + map[string]interface{}{ + "name": "bar", + }, + }, + &Target{ + Name: "bar", + }, + nil, + }, + + "variables": { + []interface{}{ + map[string]interface{}{ + "name": "{{user `name`}}", + }, + map[string]interface{}{ + "packer_user_variables": map[string]string{ + "name": "bar", + }, + }, + }, + &Target{ + Name: "bar", + }, + nil, + }, + + "filter": { + []interface{}{ + map[string]interface{}{ + "name": "{{user `name`}}", + "address": "{{user `name`}}", + }, + map[string]interface{}{ + "packer_user_variables": map[string]string{ + "name": "bar", + }, + }, + }, + &Target{ + Name: "bar", + Address: "{{user `name`}}", + }, + &DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Include: []string{"name"}, + }, + }, + }, + } + + for k, tc := range cases { + var result Target + err := Decode(&result, tc.Opts, tc.Input...) + if err != nil { + t.Fatalf("err: %s\n\n%s", k, err) + } + + if !reflect.DeepEqual(&result, tc.Output) { + t.Fatalf("bad:\n\n%#v\n\n%#v", &result, tc.Output) + } + } +}