func TestFnGetAtt_Passthru_NonStringArguments(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{ "FakeResource": map[string]interface{}{ "FakeProperty": "FakeValue", }, })) templateRules := template.Rules{} inputs := []interface{}{ []interface{}{"FakeResource", 1}, []interface{}{1, "FakeProperty"}, } for _, input := range inputs { fnGetAtt := MakeFnGetAtt(&stack, &templateRules) input := interface{}(map[string]interface{}{ "Fn::GetAtt": input, }) newKey, newNode := fnGetAtt([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("FnGetAtt modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, input) { t.Fatalf("FnGetAtt with non-string arguments modified the data (%#v instead of %#v)", newNode, input) } } }
func TestFnGetAtt_Basic(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{ "FakeResource": map[string]interface{}{ "FakeProperty": map[string]interface{}{"FakeSub": "FakeValue"}, }, })) templateRules := template.Rules{} inputs := []interface{}{ []interface{}{"FakeResource", "FakeProperty"}, []interface{}{"FakeResource", "FakeProperty.FakeSub"}, } expected := []interface{}{ map[string]interface{}{"FakeSub": "FakeValue"}, "FakeValue", } for i, input := range inputs { fnGetAtt := MakeFnGetAtt(&stack, &templateRules) input := interface{}(map[string]interface{}{ "Fn::GetAtt": input, }) newKey, newNode := fnGetAtt([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("FnGetAtt modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, expected[i]) { t.Fatalf("FnGetAtt for %v did not return the expected result (%#v instead of %#v)", input, newNode, expected[i]) } } }
// With no aliases involved, returns nothing (to prevent infinite recursion) func TestGetBasic(t *testing.T) { i := DeepAlias{fallbackmap.DeepMap(map[string]interface{}{ "foo": 1, "bar": 2, })} testGetNil(i, []string{"foo"}, t) testGetNil(i, []string{"bar"}, t) }
func TestGetAliased(t *testing.T) { i := DeepAlias{fallbackmap.DeepMap(map[string]interface{}{ "foo": 1, "bar": 2, "anAlias": "foo", "anotherAlias": "bar", })} testGetInt(i, []string{"[anAlias]"}, 1, t) testGetInt(i, []string{"[anotherAlias]"}, 2, t) }
func MakeFnWith(sources *deepstack.DeepStack, outerRules *template.Rules) template.Rule { return func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } raw, ok := singleKey(node, "Fn::With") if !ok { return key, node //passthru } args, ok := collectArgs( raw, func(argsSoFar []interface{}) bool { return len(argsSoFar) < 2 }, func(argsSoFar []interface{}, arg interface{}) (bool, interface{}) { // unconditionally process the argument, in case it needs to be skipped key, node := template.Walk(path, arg, outerRules) if skip, ok := key.(bool); ok && skip { return true, nil } if len(argsSoFar) == 1 { return false, arg // return unprocessed 2nd arg. It's a template. } return false, node }, ) if !ok { return key, node //passthru } var source map[string]interface{} if source, ok = args[0].(map[string]interface{}); !ok { return key, node //passthru } sources.Push(fallbackmap.DeepMap(source)) innerTemplate := interface{}(args[1]) key, generated := template.Walk(path, innerTemplate, outerRules) sources.PopDiscard() return key, interface{}(generated) } }
func TestGetDeepAlias(t *testing.T) { i := DeepAlias{fallbackmap.DeepMap(map[string]interface{}{ "foo": 1, "a": map[string]interface{}{ "aa": 2, "ab": map[string]interface{}{ "aba": 3, "anAlias": "bar", }, "bar": 4, }, })} testGetInt(i, []string{"a", "[a.ab.anAlias]"}, 4, t) }
// With no aliases involved, returns nothing (to prevent infinite recursion) func TestGetDeepBasic(t *testing.T) { i := DeepAlias{fallbackmap.DeepMap(map[string]interface{}{ "foo": 1, "a": map[string]interface{}{ "aa": 2, "ab": map[string]interface{}{ "aba": 3, "abb": 4, }, }, })} testGetNil(i, []string{"a", "aa"}, t) testGetNil(i, []string{"a", "ab", "aba"}, t) }
func (catalogue *DeepCloudFormationOutputs) Get(path []string) (interface{}, bool) { // path should always be in the form: [StackName, "Outputs", OutputParameter] if len(path) != 3 || !isValidStackName(path[0]) || path[1] != "Outputs" || !isValidOutputName(path[2]) { return nil, false } if catalogue.cache != nil { cached, ok := catalogue.cache[path[0]] if ok { return cached.Get(path[1:]) } } svc := cloudformation.New(&aws.Config{Region: aws.String(catalogue.Region)}) description, err := svc.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: &path[0], }) if err != nil { // FIXME: need a better way to handle this. // Right now we just ignore the error, as we may not have actually wanted a // Stack Output fmt.Fprintf(os.Stderr, "Err: %s\n", err) return nil, false } if len(description.Stacks) != 1 { panic(fmt.Sprintf( "Description of [%s] did not return in exactly one Stack", path[0], )) } outputs := map[string]interface{}{} stack := description.Stacks[0] for _, output := range stack.Outputs { outputs[*output.OutputKey] = *output.OutputValue } deep := fallbackmap.DeepMap(map[string]interface{}{ "Outputs": outputs, }) catalogue.cache[path[0]] = deep return deep.Get(path[1:]) }
func Test_Lazy_traversesTemplateRuleResults(t *testing.T) { testRules := template.Rules{} testRules.Attach(func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } if key == "replaceThis" { return key, map[string]interface{}{ "replacedDeep": "replacedDeepValue", } } return key, node }) lazy := NewLazyMap(fallbackmap.DeepMap(map[string]interface{}{ "a": "anA", "b": interface{}(map[string]interface{}{ "innerA": "anInnerA", "replaceThis": "this should be replaced", }), "replaceThis": "this should also be replaced", }), &testRules) for path, expected := range map[string]string{ "b.replaceThis.replacedDeep": "replacedDeepValue", "replaceThis.replacedDeep": "replacedDeepValue", } { result, has_key := lazy.Get(strings.Split(path, ".")) if !has_key { t.Fatalf("Get() of a rule-modified map did not claim to have a key for path %#v", path) } resultString, ok := result.(string) if !ok { t.Fatalf("Get() of a rule-modified map did not return the correct type of value for path %#v", path) } if resultString != expected { t.Fatalf("Get() of a rule-modified map did not return the expected value '%s' (got '%s' instead) for path %#v", expected, resultString, path) } } }
func TestFnGetAtt_ProcessBound(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{ "FakeResource": map[string]interface{}{ "FakeProperty": map[string]interface{}{"FakeSub": "FakeValue"}, }, })) templateRules := template.Rules{} templateRules.Attach(func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } if stringVal, ok := node.(string); ok && stringVal == "FakeValue" { return key, interface{}("ProcessedFakeValue") } return key, node }) inputs := []interface{}{ []interface{}{"FakeResource", "FakeProperty.FakeSub"}, } expected := []interface{}{ "ProcessedFakeValue", } for i, input := range inputs { fnGetAtt := MakeFnGetAtt(&stack, &templateRules) input := interface{}(map[string]interface{}{ "Fn::GetAtt": input, }) newKey, newNode := fnGetAtt([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("FnGetAtt modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, expected[i]) { t.Fatalf("FnGetAtt for %v did not return the expected result (%#v instead of %#v)", input, newNode, expected[i]) } } }
func (catalogue *DeepCloudFormationResources) Get(path []string) (interface{}, bool) { // path should always be in the form: [StackName, "Outputs", OutputParameter] if len(path) != 3 || !isValidStackName(path[0]) || path[1] != "Resources" || !isValidResourceName(path[2]) { return nil, false } if catalogue.cache != nil { cached, ok := catalogue.cache[path[0]] if ok { return cached.Get(path[1:]) } } svc := cloudformation.New(&aws.Config{Region: aws.String(catalogue.Region)}) response, err := svc.DescribeStackResources(&cloudformation.DescribeStackResourcesInput{ StackName: &path[0], }) if err != nil { // FIXME: need a better way to handle this. // Right now we just ignore the error, as we may not have actually wanted a // Stack Resource fmt.Fprintf(os.Stderr, "Err: %s\n", err) return nil, false } resources := map[string]interface{}{} for _, resource := range response.StackResources { resources[*resource.LogicalResourceId] = *resource.PhysicalResourceId } deep := fallbackmap.DeepMap(map[string]interface{}{ "Resources": resources, }) catalogue.cache[path[0]] = deep return deep.Get(path[1:]) }
func TestRef_Basic(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{ "BoundVar": "BoundValue", })) templateRules := template.Rules{} expected := "BoundValue" ref := MakeRef(&stack, &templateRules) input := interface{}(map[string]interface{}{ "Ref": "BoundVar", }) newKey, newNode := ref([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("Ref modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, expected) { t.Fatalf("Ref for %v did not return the expected result (%#v instead of %#v)", input, newNode, expected) } }
func Test_Stack(t *testing.T) { stack := DeepStack{} frameOne := fallbackmap.DeepMap(map[string]interface{}{ "a": "one:a", "b": "one:b", }) frameTwo := fallbackmap.DeepMap(map[string]interface{}{ "a": "two:a", "d": "two:d", }) stack.Push(frameOne) stack.Push(frameTwo) value, _ := stack.Get([]string{"a"}) if value != "two:a" { t.Fatalf("overridden 'a' value not retrieved (%#v instead of %#v)", value, "two:a") } value, _ = stack.Get([]string{"d"}) if value != "two:d" { t.Fatalf("appended 'd' value not retrieved (%#v instead of %#v)", value, "two:d") } value, _ = stack.Get([]string{"b"}) if value != "one:b" { t.Fatalf("original 'b' value not retrieved (%#v instead of %#v)", value, "one:b") } _, hasKey := stack.Get([]string{"c"}) if hasKey { t.Fatalf("undefined 'c' value claims to be set") } aFrame := stack.Pop() if !reflect.DeepEqual(aFrame, frameTwo) { t.Fatalf("Pop did not return the most-recent Frame (%#v instead)", aFrame) } value, _ = stack.Get([]string{"a"}) if value != "one:a" { t.Fatalf("original 'a' value not retrieved after Pop (%#v instead of %#v)", value, "one:a") } _, hasKey = stack.Get([]string{"d"}) if hasKey { t.Fatalf("appended 'd' value still present after Pop") } stack.Push(fallbackmap.DeepMap(frameTwo)) value, _ = stack.Get([]string{"a"}) if value != "two:a" { t.Fatalf("overridden 'a' value not retrieved [again] (%#v instead of %#v)", value, "two:a") } stack.PopDiscard() value, _ = stack.Get([]string{"a"}) if value != "one:a" { t.Fatalf("original 'a' value not retrieved after PopDiscard (%#v instead of %#v)", value, "one:a") } }
func (f *InputsFlag) Set(parametersFilename string) (err error) { var inputStream io.Reader var raw interface{} var ok bool var gotRaw bool var ins []interface{} var in map[string]interface{} rawJson := strings.NewReader(parametersFilename) rawDecoder := json.NewDecoder(rawJson) if err := rawDecoder.Decode(&raw); err == nil { if ins, ok = raw.([]interface{}); ok { gotRaw = true } else if in, ok = raw.(map[string]interface{}); ok { ins = append(ins, interface{}(in)) gotRaw = true } } if gotRaw { parametersFilename = "[inline]" } else { // reset, as we cannot rely on the state of raw after decoding into it raw = interface{}(nil) if inputStream, err = os.Open(parametersFilename); err != nil { return err } inputDecoder := json.NewDecoder(inputStream) if err := inputDecoder.Decode(&raw); err != nil { return err } if ins, ok = raw.([]interface{}); !ok { if in, ok = raw.(map[string]interface{}); !ok { return fmt.Errorf("JSON data does not decode into an array or map") } ins = append(ins, interface{}(in)) } } var i int for i, raw = range ins { if in, ok = raw.(map[string]interface{}); !ok { return fmt.Errorf("JSON data does not decode into a map or array of maps") } var parametersFilespec string if len(ins) == 1 { parametersFilespec = parametersFilename } else { parametersFilespec = fmt.Sprintf("%s[%d]", parametersFilename, i) } f.inputs.Override(lazymap.NewLazyMap(fallbackmap.DeepMap(in), f.rules)) f.sources = append(f.sources, inputSource{filename: parametersFilespec, data: in}) } return nil }
func main() { templateRules := template.Rules{} inputParameters := NewInputsFlag(&templateRules) var templateFilename string var outputWhat OutputWhatFlag flag.StringVar(&templateFilename, "template", "-", "CloudFormation Template to process") flag.Var(&inputParameters, "parameters", "File to use of input parameters (can be specified multiple times)") flag.Var(&outputWhat, "output", "What to output after processing the Template") flag.Parse() var jsonStream io.Reader var err error if templateFilename == "-" { jsonStream = os.Stdin } else if jsonStream, err = os.Open(templateFilename); err != nil { panic(err) } dec := json.NewDecoder(jsonStream) t := make(map[string]interface{}) if err := dec.Decode(&t); err != nil { panic(err) } sources := fallbackmap.FallbackMap{} stack := deepstack.DeepStack{} sources.Attach(inputParameters.Get()) sources.Attach(deepalias.DeepAlias{&stack}) sources.Attach(deepcloudformationoutputs.NewDeepCloudFormationOutputs("eu-west-1")) sources.Attach(deepcloudformationresources.NewDeepCloudFormationResources("eu-west-1")) stack.Push(&sources) templateRules.AttachEarly(rules.ExcludeComments) templateRules.AttachEarly(rules.MakeFnFor(&stack, &templateRules)) templateRules.AttachEarly(rules.MakeFnWith(&stack, &templateRules)) templateRules.Attach(rules.FnAdd) templateRules.Attach(rules.FnIf) templateRules.Attach(rules.FnAnd) templateRules.Attach(rules.FnOr) templateRules.Attach(rules.FnNot) templateRules.Attach(rules.FnEquals) templateRules.Attach(rules.FnConcat) templateRules.Attach(rules.FnFromEntries) templateRules.Attach(rules.FnHasKey) templateRules.Attach(rules.FnJoin) templateRules.Attach(rules.FnKeys) templateRules.Attach(rules.FnLength) templateRules.Attach(rules.FnMerge) templateRules.Attach(rules.FnMergeDeep) templateRules.Attach(rules.FnMod) templateRules.Attach(rules.FnSplit) templateRules.Attach(rules.FnToEntries) templateRules.Attach(rules.FnUnique) templateRules.Attach(rules.MakeFnGetAtt(&stack, &templateRules)) templateRules.Attach(rules.MakeRef(&stack, &templateRules)) templateRules.Attach(rules.MakeFnHasRef(&stack)) templateRules.Attach(rules.MakeFnIncludeFile(vfs.OS("/"), &templateRules)) templateRules.Attach(rules.MakeFnIncludeFileRaw(vfs.OS("/"))) templateRules.Attach(rules.ReduceConditions) // First Pass (to collect Parameter names) processed := template.Process(t, &templateRules) parameterRefs := map[string]interface{}{} if processedMap, ok := processed.(map[string]interface{}); ok { if processedParameters, ok := processedMap["Parameters"]; ok { if processedParametersMap, ok := processedParameters.(map[string]interface{}); ok { for parameterName, _ := range processedParametersMap { parameterRefs[parameterName] = map[string]interface{}{ "ParamRef": parameterName, } } } } } stack.Push(fallbackmap.DeepMap(parameterRefs)) templateRules.Attach(func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } if nodeMap, ok := node.(map[string]interface{}); !ok || len(nodeMap) != 1 { return key, node //passthru } if refName, ok := node.(map[string]interface{})["ParamRef"]; ok { return key, interface{}(map[string]interface{}{"Ref": interface{}(refName)}) } return key, node }) processed = template.Process(t, &templateRules) stack.PopDiscard() switch outputWhat.Get().what { case OutputTemplate: enc := json.NewEncoder(os.Stdout) enc.Encode(processed) case OutputCredentials: credentials := []interface{}{} credentialMap := make(map[string]interface{}) for _, input := range inputParameters.Sources() { if !outputWhat.Get().hasKey || outputWhat.Get().key == input.filename { credentialMap = template.Process(input.data, &templateRules).(map[string]interface{}) credentialMap["$comment"] = map[string]interface{}{"filename": input.filename} credentials = append(credentials, credentialMap) } } if len(credentials) == 0 && outputWhat.Get().hasKey { panic(fmt.Errorf("No parameters file '%s' was input", outputWhat.Get().key)) } enc := json.NewEncoder(os.Stdout) if len(credentials) == 1 { enc.Encode(credentials[0]) } else { enc.Encode(credentials) } case OutputParameters: parameters := []cloudformation.Parameter{} for name, _ := range parameterRefs { value, ok := sources.Get([]string{name}) if !ok { continue } value = template.Process(value, &templateRules) parameters = append(parameters, func(name string, value interface{}) cloudformation.Parameter { stringval := fmt.Sprintf("%s", value) boolval := false return cloudformation.Parameter{ ParameterKey: &name, ParameterValue: &stringval, UsePreviousValue: &boolval, } }(name, value)) } enc := json.NewEncoder(os.Stdout) enc.Encode(parameters) } }
func MakeFnFor(sources *deepstack.DeepStack, templateRules *template.Rules) template.Rule { return func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } raw, ok := singleKey(node, "Fn::For") if !ok { return key, node //passthru } args, ok := collectArgs( raw, func(argsSoFar []interface{}) bool { return len(argsSoFar) < 3 }, func(argsSoFar []interface{}, arg interface{}) (skip bool, newNode interface{}) { // unconditionally process the argument, in case it needs to be skipped key, node := template.Walk(path, arg, templateRules) if skip, ok := key.(bool); ok && skip { return true, nil } if len(argsSoFar) == 2 { return false, arg // return unprocessed 3rd arg. It's a template. } return false, node }, ) if !ok { return key, node //passthru } var refNames []interface{} var refName interface{} if refNames, ok = args[0].([]interface{}); ok { if len(refNames) == 1 { refNames = []interface{}{nil, refNames[0]} } else if len(refNames) != 2 { return key, node //passthru } } else { refNames = []interface{}{nil, args[0]} } for _, refName = range refNames { if _, ok = refName.(string); !ok && refName != nil { return key, node //passthru } } valuesInterface := args[1] var values []interface{} if values, ok = valuesInterface.([]interface{}); !ok { return key, node //passthru } loopTemplate := interface{}(args[2]) generated := []interface{}{} for deepIndex, value := range values { refMap := make(map[string]interface{}) if refNames[0] != nil { refMap[refNames[0].(string)] = float64(deepIndex) } if refNames[1] != nil { refMap[refNames[1].(string)] = value } deepPath := make([]interface{}, len(path)+1) copy(deepPath, path) deepPath[cap(deepPath)-1] = interface{}(deepIndex) sources.Push(fallbackmap.DeepMap(refMap)) newIndex, processed := template.Walk(deepPath, loopTemplate, templateRules) sources.PopDiscard() if newIndex != nil { generated = append(generated, processed) } } return key, interface{}(generated) } }
func TestFnFor_StackWithArray(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{"outer": "outerValue", "masked": "outerMasked"})) testRefNames := []interface{}{ []interface{}{"key", "masked"}, []interface{}{nil, "masked"}, []interface{}{"key", nil}, []interface{}{nil, nil}, []interface{}{"masked"}, "masked", } expected := []interface{}{ []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"innerMasking", true}, "key": []interface{}{float64(0), true}, }, }, []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"innerMasking", true}, }, }, []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"outerMasked", true}, "key": []interface{}{float64(0), true}, }, }, []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"outerMasked", true}, }, }, []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"innerMasking", true}, }, }, []interface{}{ map[string]interface{}{ "outer": []interface{}{"outerValue", true}, "masked": []interface{}{"innerMasking", true}, }, }, } for i, refNames := range testRefNames { input := interface{}(map[string]interface{}{ "Fn::For": []interface{}{ refNames, []interface{}{"innerMasking"}, "aTemplate", }, }) templateRules := template.Rules{} templateRules.Attach(func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } if stringVal, ok := node.(string); !ok || stringVal != "aTemplate" { return key, node } generated := map[string]interface{}{} for binding, _ := range expected[i].([]interface{})[0].(map[string]interface{}) { value, has_key := stack.Get([]string{binding}) generated[binding] = []interface{}{value, has_key} } return key, generated }) fnFor := MakeFnFor(&stack, &templateRules) newKey, newNode := fnFor([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("FnFor modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, expected[i]) { t.Fatalf("FnFor did not have the correct stack values with refNames %v during templateRule (%#v instead of %#v)", refNames, newNode, expected[i], ) } } }
func TestFnWith_StackWithArray(t *testing.T) { stack := deepstack.DeepStack{} stack.Push(fallbackmap.DeepMap(map[string]interface{}{"outer": "outer-value", "masked": "masked-value"})) input := interface{}(map[string]interface{}{ "Fn::With": []interface{}{ map[string]interface{}{ "masked": "masking-value", "inner": "inner-value", }, map[string]interface{}{ "outer": "replace-with-outer", "masked": "replace-with-masked", "inner": "replace-with-inner", "untouched": "stay-the-same", }, }, }) expected := interface{}(map[string]interface{}{ "outer": "outer-value", "masked": "masking-value", "inner": "inner-value", "untouched": "stay-the-same", }) templateRules := template.Rules{} templateRules.Attach(func(path []interface{}, node interface{}) (interface{}, interface{}) { key := interface{}(nil) if len(path) > 0 { key = path[len(path)-1] } newNode := make(map[string]interface{}) if nodeMap, ok := node.(map[string]interface{}); ok { if _, ok := node.(map[string]interface{})["untouched"]; ok { for key, value := range nodeMap { newValue, hasKey := stack.Get([]string{key}) if hasKey { newNode[key] = newValue } else { newNode[key] = value } } return key, newNode } } return key, node }) fnWith := MakeFnWith(&stack, &templateRules) newKey, newNode := fnWith([]interface{}{"x", "y"}, input) if newKey != "y" { t.Fatalf("FnWith modified the path (%v instead of %v)", newKey, "y") } if !reflect.DeepEqual(newNode, expected) { t.Fatalf("FnWith did not have the correct stack values during templateRule (%#v instead of %#v)", newNode, expected, ) } }