Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions reposerver/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,31 @@ func (s *Service) createGetAppDetailsCacheHandler(res *apiclient.RepoAppDetailsR
}
}

// flattenValues flattens nested map to dot-notation keys for helm parameters
func flattenValues(input map[string]any, output map[string]string, prefix string) {
for k, v := range input {
key := k
if prefix != "" {
key = prefix + "." + k
}
switch val := v.(type) {
case map[string]any:
flattenValues(val, output, key)
case []any:
for i, item := range val {
switch itemVal := item.(type) {
case map[string]any:
flattenValues(itemVal, output, fmt.Sprintf("%s[%d]", key, i))
default:
output[fmt.Sprintf("%s[%d]", key, i)] = fmt.Sprintf("%v", item)
}
}
default:
output[key] = fmt.Sprintf("%v", val)
}
}
}

func populateHelmAppDetails(res *apiclient.RepoAppDetailsResponse, appPath string, repoRoot string, q *apiclient.RepoServerAppDetailsQuery, gitRepoPaths utilio.TempPaths) error {
var selectedValueFiles []string
var availableValueFiles []string
Expand Down Expand Up @@ -2281,6 +2306,26 @@ func populateHelmAppDetails(res *apiclient.RepoAppDetailsResponse, appPath strin
if err != nil {
return err
}

// Merge user's inline values into params (overrides chart defaults)
if q.Source.Helm != nil && !q.Source.Helm.ValuesIsEmpty() {
var raw any
if err := yaml.Unmarshal(q.Source.Helm.ValuesYAML(), &raw); err != nil {
log.Warnf("failed to parse helm values: %v", err)
} else if userVals, ok := raw.(map[string]any); ok {
flattenValues(userVals, params, "")
} else if raw != nil {
log.Warnf("helm values is not a map, got %T", raw)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a warning/log message when spec.source.helm.values is not a valid Helm values object?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added warning log when helm values parsing fails - will now log failed to parse helm values: <error> if the inline values YAML is malformed.


// Merge --set style parameter overrides (highest precedence)
if q.Source.Helm != nil {
for _, p := range q.Source.Helm.Parameters {
params[p.Name] = p.Value
}
}

for k, v := range params {
res.Helm.Parameters = append(res.Helm.Parameters, &v1alpha1.HelmParameter{
Name: k,
Expand Down
146 changes: 146 additions & 0 deletions reposerver/repository/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4604,3 +4604,149 @@ func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) {
// verify that newGitClient was never invoked
assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources")
}

func TestFlattenValues(t *testing.T) {
tests := []struct {
name string
input map[string]any
expected map[string]string
}{
{
name: "simple values",
input: map[string]any{"replicaCount": 3, "debug": true},
expected: map[string]string{"replicaCount": "3", "debug": "true"},
},
{
name: "nested values",
input: map[string]any{"image": map[string]any{"tag": "1.0", "pullPolicy": "Always"}},
expected: map[string]string{"image.tag": "1.0", "image.pullPolicy": "Always"},
},
{
name: "deeply nested",
input: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value"}}},
expected: map[string]string{"a.b.c": "value"},
},
{
name: "array values",
input: map[string]any{"hosts": []any{"host1", "host2"}},
expected: map[string]string{"hosts[0]": "host1", "hosts[1]": "host2"},
},
{
name: "empty map",
input: map[string]any{},
expected: map[string]string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := make(map[string]string)
flattenValues(tt.input, output, "")
assert.Equal(t, tt.expected, output)
})
}
}

func Test_populateHelmAppDetails_WithInlineValues(t *testing.T) {
emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
res := apiclient.RepoAppDetailsResponse{}
q := apiclient.RepoServerAppDetailsQuery{
Repo: &v1alpha1.Repository{},
Source: &v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "replicaCount: 5\nimage:\n tag: \"1.25.0\"",
},
},
}
appPath, err := filepath.Abs("./testdata/values-files/")
require.NoError(t, err)
err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
require.NoError(t, err)

// Check that inline values are merged into parameters
paramMap := make(map[string]string)
for _, p := range res.Helm.Parameters {
paramMap[p.Name] = p.Value
}
assert.Equal(t, "5", paramMap["replicaCount"])
assert.Equal(t, "1.25.0", paramMap["image.tag"])
}

func Test_populateHelmAppDetails_WithParameterOverrides(t *testing.T) {
emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
res := apiclient.RepoAppDetailsResponse{}
q := apiclient.RepoServerAppDetailsQuery{
Repo: &v1alpha1.Repository{},
Source: &v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "replicaCount: 5",
Parameters: []v1alpha1.HelmParameter{
{Name: "replicaCount", Value: "10"},
{Name: "image.tag", Value: "2.0.0"},
},
},
},
}
appPath, err := filepath.Abs("./testdata/values-files/")
require.NoError(t, err)
err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
require.NoError(t, err)

// Check that --set parameters override inline values
paramMap := make(map[string]string)
for _, p := range res.Helm.Parameters {
paramMap[p.Name] = p.Value
}
// Parameters should override inline values
assert.Equal(t, "10", paramMap["replicaCount"])
assert.Equal(t, "2.0.0", paramMap["image.tag"])
}

func Test_populateHelmAppDetails_WithInvalidValues(t *testing.T) {
emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
appPath, err := filepath.Abs("./testdata/values-files/")
require.NoError(t, err)

t.Run("top-level array should not panic", func(t *testing.T) {
res := apiclient.RepoAppDetailsResponse{}
q := apiclient.RepoServerAppDetailsQuery{
Repo: &v1alpha1.Repository{},
Source: &v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "- item1\n- item2",
},
},
}
// Should not panic, just skip the invalid values
err := populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
require.NoError(t, err)
})

t.Run("scalar value should not panic", func(t *testing.T) {
res := apiclient.RepoAppDetailsResponse{}
q := apiclient.RepoServerAppDetailsQuery{
Repo: &v1alpha1.Repository{},
Source: &v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "just a string",
},
},
}
err := populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
require.NoError(t, err)
})

t.Run("malformed yaml should not panic", func(t *testing.T) {
res := apiclient.RepoAppDetailsResponse{}
q := apiclient.RepoServerAppDetailsQuery{
Repo: &v1alpha1.Repository{},
Source: &v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "key: [invalid yaml",
},
},
}
err := populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
require.NoError(t, err)
})
}
Loading