diff --git a/api/v1alpha1/release.go b/api/v1alpha1/release.go index 3e5230b..5f4a27f 100644 --- a/api/v1alpha1/release.go +++ b/api/v1alpha1/release.go @@ -25,10 +25,11 @@ type ReleaseMetadata struct { } type ReleaseChart struct { - Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` - Version string `yaml:"version,omitempty" json:"version,omitempty"` - Name string `yaml:"name,omitempty" json:"name,omitempty"` - RepoUrl string `yaml:"repoUrl,omitempty" json:"repoUrl,omitempty"` + Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + RepoUrl string `yaml:"repoUrl,omitempty" json:"repoUrl,omitempty"` + Mappings map[string]any `yaml:"mappings,omitempty" json:"mappings,omitempty"` } func (chart ReleaseChart) Validate(validRefs []string) error { diff --git a/internal/release/render/render.go b/internal/release/render/render.go index 0f6d72a..1816627 100644 --- a/internal/release/render/render.go +++ b/internal/release/render/render.go @@ -128,6 +128,10 @@ func HydrateValues(release *v1alpha1.Release, chart *helm.ChartFS, mappings *con return nil, fmt.Errorf("hydrating object values: %w", err) } + for key, value := range chart.Mappings { + setInMap(values, splitIntoPathSegments(key), value) + } + if mappings != nil && !slices.Contains(mappings.ReleaseIgnoreList, release.Name) { for mapping, value := range mappings.Mappings { setInMap(values, splitIntoPathSegments(mapping), value) diff --git a/internal/release/render/render_test.go b/internal/release/render/render_test.go index 44edb1f..2b6841d 100644 --- a/internal/release/render/render_test.go +++ b/internal/release/render/render_test.go @@ -21,6 +21,7 @@ import ( func TestRenderRelease(t *testing.T) { type RenderTestParams struct { Release *v1alpha1.Release + Chart helm.Chart ChartFS func(*xfs.FSMock) func(*testing.T) IO internal.IO SetupHelmMock func(*helm.PullRendererMock) func(*testing.T) @@ -70,7 +71,7 @@ func TestRenderRelease(t *testing.T) { t, helm.RenderOpts{ ReleaseName: "release", - Values: map[string]interface{}{}, + Values: map[string]any{}, ChartPath: "path/to/chart", }, mock.RenderCalls()[0].Opts, @@ -165,8 +166,8 @@ func TestRenderRelease(t *testing.T) { t, helm.RenderOpts{ ReleaseName: "release", - Values: map[string]interface{}{ - "corsOrigins": []interface{}{"origin1.com", "origin2.com"}, + Values: map[string]any{ + "corsOrigins": []any{"origin1.com", "origin2.com"}, "env": "env", "version": "v1.2.3", }, @@ -178,6 +179,78 @@ func TestRenderRelease(t *testing.T) { }, }, }, + { + Name: "with chart level mappings", + Params: RenderTestParams{ + Release: func() *v1alpha1.Release { + rel := buildRelease("env", "release") + rel.Spec.Values = map[string]any{} + rel.Spec.Version = "v9.9.9" + return rel + }(), + Chart: helm.Chart{ + Mappings: map[string]any{ + "test.mapping": "{{ .Release.Spec.Version }}", + }, + }, + SetupHelmMock: func(mock *helm.PullRendererMock) func(*testing.T) { + return func(t *testing.T) { + require.Len(t, mock.RenderCalls(), 1) + require.Equal( + t, + helm.RenderOpts{ + ReleaseName: "release", + Values: map[string]any{ + "test": map[string]any{ + "mapping": "v9.9.9", + }, + }, + ChartPath: "path/to/chart", + }, + mock.RenderCalls()[0].Opts, + ) + } + }, + ValueMapping: &config.ValueMapping{}, + }, + }, + { + Name: "chart mappings take priority over global mappings", + Params: RenderTestParams{ + Release: func() *v1alpha1.Release { + rel := buildRelease("env", "release") + rel.Spec.Values = map[string]any{} + rel.Spec.Version = "v9.9.9" + return rel + }(), + Chart: helm.Chart{ + Mappings: map[string]any{"test.mapping": "{{ .Release.Spec.Version }}"}, + }, + SetupHelmMock: func(mock *helm.PullRendererMock) func(*testing.T) { + return func(t *testing.T) { + require.Len(t, mock.RenderCalls(), 1) + require.Equal( + t, + helm.RenderOpts{ + ReleaseName: "release", + Values: map[string]any{ + "test": map[string]any{ + "mapping": "v9.9.9", + }, + }, + ChartPath: "path/to/chart", + }, + mock.RenderCalls()[0].Opts, + ) + } + }, + ValueMapping: &config.ValueMapping{ + Mappings: map[string]any{ + "test.mapping": "GLOBAL", + }, + }, + }, + }, } for _, tc := range cases { @@ -211,7 +284,8 @@ func TestRenderRelease(t *testing.T) { result, err := Render(context.Background(), RenderParams{ Release: tc.Params.Release, Chart: &helm.ChartFS{ - FS: fsMock, + Chart: tc.Params.Chart, + FS: fsMock, }, ValueMapping: tc.Params.ValueMapping, Helm: helmMock, diff --git a/pkg/helm/cache.go b/pkg/helm/cache.go index b83316b..f49b928 100644 --- a/pkg/helm/cache.go +++ b/pkg/helm/cache.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "maps" "net/url" "os" "path" @@ -24,9 +25,10 @@ type ChartCache struct { } type Chart struct { - RepoURL string `yaml:"repoUrl"` - Name string `yaml:"name"` - Version string `yaml:"version"` + RepoURL string `yaml:"repoUrl"` + Name string `yaml:"name"` + Version string `yaml:"version"` + Mappings map[string]any `yaml:"mappings"` } func (chart Chart) ToURL() (*url.URL, error) { @@ -54,9 +56,10 @@ type ChartFS struct { func (cache ChartCache) GetReleaseChart(release *v1alpha1.Release) (Chart, error) { if repoURL := release.Spec.Chart.RepoUrl; repoURL != "" { return Chart{ - RepoURL: repoURL, - Name: release.Spec.Chart.Name, - Version: release.Spec.Chart.Version, + RepoURL: repoURL, + Name: release.Spec.Chart.Name, + Version: release.Spec.Chart.Version, + Mappings: release.Spec.Chart.Mappings, }, nil } @@ -70,6 +73,13 @@ func (cache ChartCache) GetReleaseChart(release *v1alpha1.Release) (Chart, error chart.Version, ) + chart.Mappings = func() map[string]any { + mappings := make(map[string]any) + maps.Copy(mappings, chart.Mappings) + maps.Copy(mappings, release.Spec.Chart.Mappings) + return mappings + }() + return chart, nil }