Fix values templating bug where mergeOverwrite mutated global values
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									3f5d4110f6
								
							
						
					
					
						commit
						052a2a8219
					
				|  | @ -396,7 +396,11 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp | |||
| 			return nil, clean, fmt.Errorf("rendering set value entry for release %s: %v", release.Name, err) | ||||
| 		} | ||||
| 		c.Opts.SetFlags = setFlags | ||||
| 		c.Opts.TemplateData = st.newReleaseTemplateData(release) | ||||
| 		templateData, err := st.newReleaseTemplateData(release) | ||||
| 		if err != nil { | ||||
| 			return nil, clean, fmt.Errorf("creating template data for release %s: %v", release.Name, err) | ||||
| 		} | ||||
| 		c.Opts.TemplateData = templateData | ||||
| 		c.Opts.TemplateFuncs = st.newReleaseTemplateFuncMap(dir) | ||||
| 
 | ||||
| 		return c, clean, nil | ||||
|  |  | |||
|  | @ -0,0 +1,130 @@ | |||
| package state | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/helmfile/helmfile/pkg/filesystem" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| // TestIssue2182_ValuesTemplatingBugFix is an integration test that reproduces
 | ||||
| // the exact scenario described in https://github.com/helmfile/helmfile/issues/2182
 | ||||
| // and verifies that our fix works correctly.
 | ||||
| func TestIssue2182_ValuesTemplatingBugFix(t *testing.T) { | ||||
| 	// Simulate the exact scenario from the issue:
 | ||||
| 	// environments:
 | ||||
| 	//   default:
 | ||||
| 	//     values:
 | ||||
| 	//       - values.yaml
 | ||||
| 	// ---
 | ||||
| 	// releases:
 | ||||
| 	//   - name: foo
 | ||||
| 	//     chart: charts/foo
 | ||||
| 	//     valuesTemplate:
 | ||||
| 	//       - {{ .Values | get "foo" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}
 | ||||
| 	//   - name: bar
 | ||||
| 	//     chart: charts/bar
 | ||||
| 	//     valuesTemplate:
 | ||||
| 	//       - {{ .Values | get "bar" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}
 | ||||
| 
 | ||||
| 	// Create test filesystem
 | ||||
| 	fs := &filesystem.FileSystem{ | ||||
| 		Glob: func(pattern string) ([]string, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 		ReadFile: func(filename string) ([]byte, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Simulate values.yaml content
 | ||||
| 	valuesYaml := map[string]any{ | ||||
| 		"global":    "shared-config", | ||||
| 		"commonKey": "commonValue", | ||||
| 		"foo": map[string]any{ | ||||
| 			"enabled":     true, | ||||
| 			"replicaCount": 2, | ||||
| 			"image":       "foo:1.0.0", | ||||
| 		}, | ||||
| 		"bar": map[string]any{ | ||||
| 			"enabled":     true, | ||||
| 			"replicaCount": 1, | ||||
| 			"image":       "bar:2.0.0", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	st := &HelmState{ | ||||
| 		fs:             fs, | ||||
| 		basePath:       "/tmp", | ||||
| 		FilePath:       "helmfile.yaml", | ||||
| 		RenderedValues: valuesYaml, | ||||
| 	} | ||||
| 
 | ||||
| 	// Define the releases as they would appear in helmfile.yaml
 | ||||
| 	fooRelease := &ReleaseSpec{ | ||||
| 		Name:  "foo", | ||||
| 		Chart: "charts/foo", | ||||
| 		ValuesTemplate: []any{ | ||||
| 			`{{ .Values | get "foo" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}`, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	barRelease := &ReleaseSpec{ | ||||
| 		Name:  "bar", | ||||
| 		Chart: "charts/bar", | ||||
| 		ValuesTemplate: []any{ | ||||
| 			`{{ .Values | get "bar" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}`, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Simulate ExecuteTemplates processing releases in order: foo then bar
 | ||||
| 	releases1 := []ReleaseSpec{*fooRelease, *barRelease} | ||||
| 	st.Releases = releases1 | ||||
| 
 | ||||
| 	result1, err := st.ExecuteTemplates() | ||||
| 	require.NoError(t, err, "ExecuteTemplates should succeed with foo then bar") | ||||
| 
 | ||||
| 	// Simulate ExecuteTemplates processing releases in reverse order: bar then foo  
 | ||||
| 	releases2 := []ReleaseSpec{*barRelease, *fooRelease} | ||||
| 	st.Releases = releases2 | ||||
| 
 | ||||
| 	result2, err := st.ExecuteTemplates() | ||||
| 	require.NoError(t, err, "ExecuteTemplates should succeed with bar then foo") | ||||
| 
 | ||||
| 	// Extract the processed releases from both executions
 | ||||
| 	fooRelease1 := result1.Releases[0] // foo from first execution (foo, bar)
 | ||||
| 	barRelease1 := result1.Releases[1] // bar from first execution (foo, bar)
 | ||||
| 
 | ||||
| 	barRelease2 := result2.Releases[0] // bar from second execution (bar, foo)
 | ||||
| 	fooRelease2 := result2.Releases[1] // foo from second execution (bar, foo)
 | ||||
| 
 | ||||
| 	// The critical assertion: Order should not matter!
 | ||||
| 	// Before the fix, the second release would see modified values from the first release
 | ||||
| 	require.Equal(t, fooRelease1.Values, fooRelease2.Values,  | ||||
| 		"foo release values should be identical regardless of processing order") | ||||
| 	require.Equal(t, barRelease1.Values, barRelease2.Values, | ||||
| 		"bar release values should be identical regardless of processing order") | ||||
| 
 | ||||
| 	// Verify that each release gets the expected merged values
 | ||||
| 	// foo release should have foo-specific values merged into the root
 | ||||
| 	fooVals1 := fooRelease1.Values[0] | ||||
| 	require.Contains(t, fooVals1, "enabled") | ||||
| 	require.Contains(t, fooVals1, "replicaCount")  | ||||
| 	require.Contains(t, fooVals1, "image") | ||||
| 	require.Contains(t, fooVals1, "global")    // Should preserve global values
 | ||||
| 	require.Contains(t, fooVals1, "commonKey") // Should preserve common values
 | ||||
| 
 | ||||
| 	// bar release should have bar-specific values merged into the root  
 | ||||
| 	barVals1 := barRelease1.Values[0] | ||||
| 	require.Contains(t, barVals1, "enabled") | ||||
| 	require.Contains(t, barVals1, "replicaCount") | ||||
| 	require.Contains(t, barVals1, "image")  | ||||
| 	require.Contains(t, barVals1, "global")    // Should preserve global values
 | ||||
| 	require.Contains(t, barVals1, "commonKey") // Should preserve common values
 | ||||
| 
 | ||||
| 	// Verify that the original values were not mutated
 | ||||
| 	originalVals := st.Values() | ||||
| 	require.Equal(t, valuesYaml, originalVals, "original values should remain unchanged") | ||||
| 
 | ||||
| 	t.Log("✅ Fix verified: Release order no longer affects values templating results") | ||||
| } | ||||
|  | @ -3167,11 +3167,14 @@ func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec, | |||
| 	return st.appendHelmXFlags(flags, release), files, nil | ||||
| } | ||||
| 
 | ||||
| func (st *HelmState) newReleaseTemplateData(release *ReleaseSpec) releaseTemplateData { | ||||
| func (st *HelmState) newReleaseTemplateData(release *ReleaseSpec) (releaseTemplateData, error) { | ||||
| 	vals := st.Values() | ||||
| 	templateData := st.createReleaseTemplateData(release, vals) | ||||
| 	templateData, err := st.createReleaseTemplateData(release, vals) | ||||
| 	if err != nil { | ||||
| 		return releaseTemplateData{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return templateData | ||||
| 	return templateData, nil | ||||
| } | ||||
| 
 | ||||
| func (st *HelmState) newReleaseTemplateFuncMap(dir string) template.FuncMap { | ||||
|  | @ -3181,7 +3184,10 @@ func (st *HelmState) newReleaseTemplateFuncMap(dir string) template.FuncMap { | |||
| } | ||||
| 
 | ||||
| func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) { | ||||
| 	templateData := st.newReleaseTemplateData(release) | ||||
| 	templateData, err := st.newReleaseTemplateData(release) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create template data for release %q: %v", release.Name, err) | ||||
| 	} | ||||
| 
 | ||||
| 	r := tmpl.NewFileRenderer(st.fs, filepath.Dir(path), templateData) | ||||
| 	rawBytes, err := r.RenderToBytes(path) | ||||
|  |  | |||
|  | @ -20,13 +20,39 @@ func (st *HelmState) Values() map[string]any { | |||
| 	return st.RenderedValues | ||||
| } | ||||
| 
 | ||||
| func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]any) releaseTemplateData { | ||||
| // deepCopyValues creates a deep copy of a values map using YAML marshal/unmarshal.
 | ||||
| // This ensures that template operations don't mutate the original values.
 | ||||
| func deepCopyValues(vals map[string]any) (map[string]any, error) { | ||||
| 	if vals == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	 | ||||
| 	serialized, err := yaml.Marshal(vals) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to deep copy values: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	var deserialized map[string]any | ||||
| 	if err := yaml.Unmarshal(serialized, &deserialized); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to deep copy values: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return deserialized, nil | ||||
| } | ||||
| 
 | ||||
| func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]any) (releaseTemplateData, error) { | ||||
| 	// Create a deep copy of values to prevent template mutations from affecting global state
 | ||||
| 	valuesCopy, err := deepCopyValues(vals) | ||||
| 	if err != nil { | ||||
| 		return releaseTemplateData{}, fmt.Errorf("failed to copy values for release %q: %v", release.Name, err) | ||||
| 	} | ||||
| 	 | ||||
| 	tmplData := releaseTemplateData{ | ||||
| 		Environment: st.Env, | ||||
| 		KubeContext: st.OverrideKubeContext, | ||||
| 		Namespace:   st.OverrideNamespace, | ||||
| 		Chart:       st.OverrideChart, | ||||
| 		Values:      vals, | ||||
| 		Values:      valuesCopy, | ||||
| 		Release: releaseTemplateDataRelease{ | ||||
| 			Name:         release.Name, | ||||
| 			Chart:        release.Chart, | ||||
|  | @ -37,7 +63,7 @@ func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[st | |||
| 		}, | ||||
| 	} | ||||
| 	tmplData.StateValues = &tmplData.Values | ||||
| 	return tmplData | ||||
| 	return tmplData, nil | ||||
| } | ||||
| 
 | ||||
| func getBoolRefFromStringTemplate(templateRef string) (*bool, error) { | ||||
|  | @ -112,7 +138,10 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) { | |||
| 
 | ||||
| 		successFlag := false | ||||
| 		for it, prev := 0, release; it < 6; it++ { | ||||
| 			tmplData := st.createReleaseTemplateData(prev, vals) | ||||
| 			tmplData, err := st.createReleaseTemplateData(prev, vals) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("failed creating template data for release \"%s\".\"%s\": %v", st.FilePath, release.Name, err) | ||||
| 			} | ||||
| 			renderer := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData) | ||||
| 			r, err := release.ExecuteTemplateExpressions(renderer) | ||||
| 			if err != nil { | ||||
|  |  | |||
|  | @ -0,0 +1,76 @@ | |||
| package state | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/helmfile/helmfile/pkg/filesystem" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| // TestValuesMutationFix reproduces and tests the fix for the issue described in
 | ||||
| // https://github.com/helmfile/helmfile/issues/2182
 | ||||
| // where mergeOverwrite modifies the global .Values object instead of creating a local copy
 | ||||
| func TestValuesMutationFix(t *testing.T) { | ||||
| 	// Create test filesystem with no files
 | ||||
| 	fs := &filesystem.FileSystem{ | ||||
| 		Glob: func(pattern string) ([]string, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 		ReadFile: func(filename string) ([]byte, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	st := &HelmState{ | ||||
| 		fs:       fs, | ||||
| 		basePath: "/tmp", | ||||
| 		RenderedValues: map[string]any{ | ||||
| 			"common": "value", | ||||
| 			"foo": map[string]any{ | ||||
| 				"specific": "foo-value", | ||||
| 			}, | ||||
| 			"bar": map[string]any{ | ||||
| 				"specific": "bar-value", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	release := &ReleaseSpec{ | ||||
| 		Name: "test-release", | ||||
| 		Chart: "test/chart", | ||||
| 	} | ||||
| 
 | ||||
| 	// Create template data twice to simulate two different releases
 | ||||
| 	vals1 := st.Values() | ||||
| 	tmplData1, err := st.createReleaseTemplateData(release, vals1) | ||||
| 	require.NoError(t, err, "first createReleaseTemplateData should not fail") | ||||
| 
 | ||||
| 	vals2 := st.Values() | ||||
| 	tmplData2, err := st.createReleaseTemplateData(release, vals2) | ||||
| 	require.NoError(t, err, "second createReleaseTemplateData should not fail") | ||||
| 
 | ||||
| 	// Verify that both template data have the same initial values
 | ||||
| 	require.Equal(t, tmplData1.Values, tmplData2.Values, "both template data should start with identical values") | ||||
| 
 | ||||
| 	// Simulate mergeOverwrite operation on first template data
 | ||||
| 	// This should not affect the second template data after our fix
 | ||||
| 	fooSection, ok := tmplData1.Values["foo"].(map[string]any) | ||||
| 	require.True(t, ok, "foo section should be a map") | ||||
| 	 | ||||
| 	// Manually perform what mergeOverwrite would do - add values from foo section to the root
 | ||||
| 	for k, v := range fooSection { | ||||
| 		tmplData1.Values[k] = v | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify that the modification only affected tmplData1, not tmplData2
 | ||||
| 	_, hasSpecificInTmpl1 := tmplData1.Values["specific"] | ||||
| 	_, hasSpecificInTmpl2 := tmplData2.Values["specific"] | ||||
| 
 | ||||
| 	require.True(t, hasSpecificInTmpl1, "tmplData1 should have the merged 'specific' key") | ||||
| 	require.False(t, hasSpecificInTmpl2, "tmplData2 should NOT have the merged 'specific' key (values should be isolated)") | ||||
| 
 | ||||
| 	// Also verify that the original values are not affected
 | ||||
| 	originalVals := st.Values() | ||||
| 	_, hasSpecificInOriginal := originalVals["specific"] | ||||
| 	require.False(t, hasSpecificInOriginal, "original Values should NOT be affected") | ||||
| } | ||||
|  | @ -0,0 +1,102 @@ | |||
| package state | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/helmfile/helmfile/pkg/filesystem" | ||||
| 	"github.com/helmfile/helmfile/pkg/tmpl" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| // TestValuesTemplateIsolation tests the fix for the helmfile values templating bug
 | ||||
| // where changing the order of releases resulted in different values being used
 | ||||
| func TestValuesTemplateIsolation(t *testing.T) { | ||||
| 	// Create test filesystem
 | ||||
| 	fs := &filesystem.FileSystem{ | ||||
| 		Glob: func(pattern string) ([]string, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 		ReadFile: func(filename string) ([]byte, error) { | ||||
| 			return nil, nil | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Create test environment values
 | ||||
| 	envValues := map[string]any{ | ||||
| 		"common": "shared-value", | ||||
| 		"foo": map[string]any{ | ||||
| 			"name": "foo-chart", | ||||
| 		}, | ||||
| 		"bar": map[string]any{ | ||||
| 			"name": "bar-chart", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	st := &HelmState{ | ||||
| 		fs:             fs, | ||||
| 		basePath:       "/tmp", | ||||
| 		RenderedValues: envValues, | ||||
| 	} | ||||
| 
 | ||||
| 	// Create two releases that use valuesTemplate with mergeOverwrite
 | ||||
| 	fooRelease := &ReleaseSpec{ | ||||
| 		Name:  "foo", | ||||
| 		Chart: "charts/foo", | ||||
| 		ValuesTemplate: []any{ | ||||
| 			"{{ .Values | get \"foo\" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	barRelease := &ReleaseSpec{ | ||||
| 		Name:  "bar", | ||||
| 		Chart: "charts/bar", | ||||
| 		ValuesTemplate: []any{ | ||||
| 			"{{ .Values | get \"bar\" (dict) | mergeOverwrite .Values | toYaml | nindent 8 }}", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Test: process foo first, then bar
 | ||||
| 	vals1 := st.Values() | ||||
| 	tmplData1, err := st.createReleaseTemplateData(fooRelease, vals1) | ||||
| 	require.NoError(t, err) | ||||
| 	 | ||||
| 	renderer1 := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData1) | ||||
| 	processedFooFirst, err := fooRelease.ExecuteTemplateExpressions(renderer1) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	vals2 := st.Values() | ||||
| 	tmplData2, err := st.createReleaseTemplateData(barRelease, vals2) | ||||
| 	require.NoError(t, err) | ||||
| 	 | ||||
| 	renderer2 := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData2) | ||||
| 	processedBarSecond, err := barRelease.ExecuteTemplateExpressions(renderer2) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	// Test: process bar first, then foo (reverse order)
 | ||||
| 	vals3 := st.Values() | ||||
| 	tmplData3, err := st.createReleaseTemplateData(barRelease, vals3) | ||||
| 	require.NoError(t, err) | ||||
| 	 | ||||
| 	renderer3 := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData3) | ||||
| 	processedBarFirst, err := barRelease.ExecuteTemplateExpressions(renderer3) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	vals4 := st.Values() | ||||
| 	tmplData4, err := st.createReleaseTemplateData(fooRelease, vals4) | ||||
| 	require.NoError(t, err) | ||||
| 	 | ||||
| 	renderer4 := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData4) | ||||
| 	processedFooSecond, err := fooRelease.ExecuteTemplateExpressions(renderer4) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	// Verify that the order doesn't matter - results should be consistent
 | ||||
| 	require.Equal(t, processedFooFirst.Values, processedFooSecond.Values,  | ||||
| 		"foo release should produce same values regardless of processing order") | ||||
| 	require.Equal(t, processedBarSecond.Values, processedBarFirst.Values, | ||||
| 		"bar release should produce same values regardless of processing order") | ||||
| 
 | ||||
| 	// Also verify that the original values are not modified
 | ||||
| 	originalVals := st.Values() | ||||
| 	require.Equal(t, envValues, originalVals,  | ||||
| 		"original values should remain unchanged") | ||||
| } | ||||
|  | @ -0,0 +1,57 @@ | |||
| package tmpl | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/helmfile/helmfile/pkg/filesystem" | ||||
| ) | ||||
| 
 | ||||
| // TestValuesIsolation reproduces the issue described in https://github.com/helmfile/helmfile/issues/2182
 | ||||
| // where mergeOverwrite modifies the global .Values object instead of creating a local copy
 | ||||
| // This test demonstrates that the issue is fixed when using the state layer (which provides isolation)
 | ||||
| func TestValuesIsolation(t *testing.T) { | ||||
| 	ctx := &Context{ | ||||
| 		fs: &filesystem.FileSystem{ | ||||
| 			Glob: func(pattern string) ([]string, error) { | ||||
| 				return nil, nil | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// Template that simulates the problematic helmfile.yaml content
 | ||||
| 	// NOTE: This test shows that the template layer itself doesn't provide isolation,
 | ||||
| 	// but the fix is implemented at the state layer where values are copied before templating
 | ||||
| 	template := ` | ||||
| {{- $originalValues := .Values }} | ||||
| {{- $fooValues := .Values | get "foo" (dict) | mergeOverwrite .Values }} | ||||
| {{- $barValues := .Values | get "bar" (dict) | mergeOverwrite .Values }} | ||||
| First render (should use original + foo): {{ $fooValues | toYaml }} | ||||
| Second render (should use original + bar): {{ $barValues | toYaml }} | ||||
| Final Values (should be original): {{ .Values | toYaml }} | ||||
| Original Values (should be same as final): {{ $originalValues | toYaml }} | ||||
| ` | ||||
| 
 | ||||
| 	data := map[string]any{ | ||||
| 		"Values": map[string]any{ | ||||
| 			"common": "value", | ||||
| 			"foo": map[string]any{ | ||||
| 				"specific": "foo-value", | ||||
| 			}, | ||||
| 			"bar": map[string]any{ | ||||
| 				"specific": "bar-value", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	buf, err := ctx.RenderTemplateToBuffer(template, data) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	result := buf.String() | ||||
| 	t.Logf("Template result:\n%s", result) | ||||
| 
 | ||||
| 	// This test still demonstrates the raw template behavior (without our fix at state layer),
 | ||||
| 	// but the actual bug is fixed at the state layer where we copy values before templating.
 | ||||
| 	// See TestValuesTemplateIsolation in pkg/state for the proper integration test.
 | ||||
| } | ||||
		Loading…
	
		Reference in New Issue