mirror of https://github.com/helm/helm
Merge 1b350cc8e9 into c3a0d3b860
commit
b4644150a4
@ -0,0 +1,369 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
release "helm.sh/helm/v4/pkg/release"
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
rspb "helm.sh/helm/v4/pkg/release/v1"
|
||||
)
|
||||
|
||||
// MergeValues is the action for intelligently merging values from multiple release revisions.
|
||||
//
|
||||
// It provides the implementation of 'helm values merge'.
|
||||
type MergeValues struct {
|
||||
cfg *Configuration
|
||||
|
||||
// Revisions specifies the revisions to merge. If empty, all revisions will be used.
|
||||
Revisions []int
|
||||
// AllRevisions indicates whether to include all revisions of the release.
|
||||
AllRevisions bool
|
||||
// OutputFormat specifies the output format (yaml, json).
|
||||
OutputFormat string
|
||||
// MergeStrategy defines how to handle conflicts between revisions.
|
||||
MergeStrategy string
|
||||
}
|
||||
|
||||
// NewMergeValues creates a new MergeValues object with the given configuration.
|
||||
func NewMergeValues(cfg *Configuration) *MergeValues {
|
||||
return &MergeValues{
|
||||
cfg: cfg,
|
||||
OutputFormat: "yaml",
|
||||
MergeStrategy: "latest", // default strategy: use latest value for conflicts
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes 'helm values merge' against the given release.
|
||||
func (m *MergeValues) Run(name string) (map[string]interface{}, error) {
|
||||
if err := m.cfg.KubeClient.IsReachable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the release history to determine which revisions to merge
|
||||
history, err := m.cfg.Releases.History(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get history for release %q: %w", name, err)
|
||||
}
|
||||
|
||||
if len(history) == 0 {
|
||||
return nil, fmt.Errorf("release %q not found", name)
|
||||
}
|
||||
|
||||
// Determine which revisions to merge
|
||||
revisionsToMerge, err := m.getRevisionsToMerge(history)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(revisionsToMerge) == 0 {
|
||||
return nil, fmt.Errorf("no revisions found to merge for release %q", name)
|
||||
}
|
||||
|
||||
// Collect values from specified revisions
|
||||
var allValues []map[string]interface{}
|
||||
var revisionInfo []string
|
||||
|
||||
for _, rev := range revisionsToMerge {
|
||||
rel, err := m.cfg.Releases.Get(name, rev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get release %q revision %d: %w", name, rev, err)
|
||||
}
|
||||
|
||||
r, err := releaserToV1Release(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var _ *rspb.Release = r // Explicitly use the type
|
||||
|
||||
// Get computed values (merged with chart defaults)
|
||||
computedVals, err := util.CoalesceValues(r.Chart, r.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not coalesce values for revision %d: %w", rev, err)
|
||||
}
|
||||
|
||||
allValues = append(allValues, computedVals)
|
||||
revisionInfo = append(revisionInfo, fmt.Sprintf("revision:%d,version:%s,updated:%s",
|
||||
rev, r.Chart.Metadata.Version, r.Info.LastDeployed.Format("2006-01-02T15:04:05Z")))
|
||||
}
|
||||
|
||||
// Merge all values using the specified strategy
|
||||
mergedValues, err := m.mergeValues(allValues)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not merge values: %w", err)
|
||||
}
|
||||
|
||||
// Add metadata about the merge operation if not empty
|
||||
if len(mergedValues) > 0 {
|
||||
if mergedValues["helm"] == nil {
|
||||
mergedValues["helm"] = map[string]interface{}{}
|
||||
}
|
||||
helmMeta := mergedValues["helm"].(map[string]interface{})
|
||||
helmMeta["mergeMetadata"] = map[string]interface{}{
|
||||
"releaseName": name,
|
||||
"revisions": revisionsToMerge,
|
||||
"revisionInfo": revisionInfo,
|
||||
"mergeStrategy": m.MergeStrategy,
|
||||
}
|
||||
}
|
||||
|
||||
return mergedValues, nil
|
||||
}
|
||||
|
||||
// getRevisionsToMerge determines which revisions to merge based on configuration
|
||||
func (m *MergeValues) getRevisionsToMerge(history []release.Releaser) ([]int, error) {
|
||||
var revisions []int
|
||||
|
||||
if m.AllRevisions {
|
||||
// Include all revisions
|
||||
for _, rel := range history {
|
||||
r, err := releaserToV1Release(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revisions = append(revisions, r.Version)
|
||||
}
|
||||
} else if len(m.Revisions) > 0 {
|
||||
// Use specified revisions
|
||||
revisions = m.Revisions
|
||||
} else {
|
||||
// Default: use all deployed revisions
|
||||
for _, rel := range history {
|
||||
r, err := releaserToV1Release(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.Info.Status == common.StatusDeployed {
|
||||
revisions = append(revisions, r.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that revisions exist
|
||||
availableVersions := make(map[int]bool)
|
||||
for _, rel := range history {
|
||||
r, err := releaserToV1Release(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var _ *rspb.Release = r // Explicitly use the type
|
||||
availableVersions[r.Version] = true
|
||||
}
|
||||
|
||||
var validRevisions []int
|
||||
for _, rev := range revisions {
|
||||
if !availableVersions[rev] {
|
||||
return nil, fmt.Errorf("revision %d does not exist for release", rev)
|
||||
}
|
||||
validRevisions = append(validRevisions, rev)
|
||||
}
|
||||
|
||||
return validRevisions, nil
|
||||
}
|
||||
|
||||
// mergeValues merges multiple values maps using the specified strategy
|
||||
func (m *MergeValues) mergeValues(valuesList []map[string]interface{}) (map[string]interface{}, error) {
|
||||
if len(valuesList) == 0 {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
if len(valuesList) == 1 {
|
||||
return valuesList[0], nil
|
||||
}
|
||||
|
||||
// Start with an empty map
|
||||
result := make(map[string]interface{})
|
||||
|
||||
switch m.MergeStrategy {
|
||||
case "latest":
|
||||
// Latest wins strategy: later values override earlier ones
|
||||
for _, values := range valuesList {
|
||||
result = util.CoalesceTables(result, values)
|
||||
}
|
||||
case "first":
|
||||
// First wins strategy: earlier values take precedence
|
||||
// We need to reverse the order and then use CoalesceTables
|
||||
for i := len(valuesList) - 1; i >= 0; i-- {
|
||||
result = util.CoalesceTables(result, valuesList[i])
|
||||
}
|
||||
case "merge":
|
||||
// Deep merge strategy: attempt to intelligently merge arrays and objects
|
||||
result = m.deepMergeValues(valuesList)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown merge strategy: %s", m.MergeStrategy)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// deepMergeValues performs a deep merge of values with intelligent conflict resolution
|
||||
func (m *MergeValues) deepMergeValues(valuesList []map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Track which keys were set by which revision for debugging
|
||||
keySources := make(map[string]int)
|
||||
|
||||
for i, values := range valuesList {
|
||||
for key, value := range values {
|
||||
if _, exists := result[key]; !exists {
|
||||
// Key doesn't exist yet, add it
|
||||
result[key] = m.deepCopyValue(value)
|
||||
keySources[key] = i + 1 // revision numbers start from 1
|
||||
} else {
|
||||
// Key exists, merge the values
|
||||
result[key] = m.mergeTwoValues(result[key], value)
|
||||
// Update source to latest revision that modified this key
|
||||
keySources[key] = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add merge metadata for debugging
|
||||
if len(result) > 0 {
|
||||
result["_mergeSources"] = keySources
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// mergeTwoValues merges two values intelligently
|
||||
func (m *MergeValues) mergeTwoValues(existing, newValue interface{}) interface{} {
|
||||
switch existing := existing.(type) {
|
||||
case map[string]interface{}:
|
||||
// If existing is a map, try to merge with new value
|
||||
if newMap, ok := newValue.(map[string]interface{}); ok {
|
||||
result := make(map[string]interface{})
|
||||
// Copy existing values
|
||||
for k, v := range existing {
|
||||
result[k] = v
|
||||
}
|
||||
// Merge new values
|
||||
for k, v := range newMap {
|
||||
if existingV, exists := result[k]; exists {
|
||||
result[k] = m.mergeTwoValues(existingV, v)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Types don't match, new value wins
|
||||
return newValue
|
||||
case []interface{}:
|
||||
// If existing is a slice, append new values if they're also slices
|
||||
if newSlice, ok := newValue.([]interface{}); ok {
|
||||
// Concatenate slices, avoiding duplicates
|
||||
result := make([]interface{}, len(existing))
|
||||
copy(result, existing)
|
||||
for _, v := range newSlice {
|
||||
if !m.sliceContains(result, v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Types don't match, new value wins
|
||||
return newValue
|
||||
default:
|
||||
// For primitive types, new value wins
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
|
||||
// sliceContains checks if a slice contains a specific value
|
||||
func (m *MergeValues) sliceContains(slice []interface{}, value interface{}) bool {
|
||||
for _, v := range slice {
|
||||
if m.valuesEqual(v, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// valuesEqual compares two values for equality
|
||||
func (m *MergeValues) valuesEqual(a, b interface{}) bool {
|
||||
// Simple equality check - could be enhanced for more complex types
|
||||
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
|
||||
}
|
||||
|
||||
// deepCopyValue creates a deep copy of a value
|
||||
func (m *MergeValues) deepCopyValue(value interface{}) interface{} {
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
result := make(map[string]interface{})
|
||||
for key, val := range v {
|
||||
result[key] = m.deepCopyValue(val)
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
result := make([]interface{}, len(v))
|
||||
for i, val := range v {
|
||||
result[i] = m.deepCopyValue(val)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRevisions parses a revision specification string into a slice of revision numbers
|
||||
func ParseRevisions(revisionSpec string) ([]int, error) {
|
||||
if revisionSpec == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var revisions []int
|
||||
parts := strings.Split(revisionSpec, ",")
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.Contains(part, "..") {
|
||||
// Handle range specification like "1..5"
|
||||
rangeParts := strings.Split(part, "..")
|
||||
if len(rangeParts) != 2 {
|
||||
return nil, fmt.Errorf("invalid revision range: %s", part)
|
||||
}
|
||||
start, err := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start revision in range %s: %w", part, err)
|
||||
}
|
||||
end, err := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid end revision in range %s: %w", part, err)
|
||||
}
|
||||
if start > end {
|
||||
return nil, fmt.Errorf("start revision %d cannot be greater than end revision %d", start, end)
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
revisions = append(revisions, i)
|
||||
}
|
||||
} else {
|
||||
// Handle single revision
|
||||
rev, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid revision number %s: %w", part, err)
|
||||
}
|
||||
revisions = append(revisions, rev)
|
||||
}
|
||||
}
|
||||
|
||||
return revisions, nil
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseRevisions(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []int
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: "1",
|
||||
expected: []int{1},
|
||||
},
|
||||
{
|
||||
input: "1,3,5",
|
||||
expected: []int{1, 3, 5},
|
||||
},
|
||||
{
|
||||
input: "1..5",
|
||||
expected: []int{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
input: "1,3..5,7",
|
||||
expected: []int{1, 3, 4, 5, 7},
|
||||
},
|
||||
{
|
||||
input: "5..1",
|
||||
expected: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "invalid",
|
||||
expected: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "1..invalid",
|
||||
expected: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result, err := ParseRevisions(tt.input)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
)
|
||||
|
||||
// newValuesCmd creates the values command
|
||||
func newValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "values",
|
||||
Short: "manage release values",
|
||||
Long: `This command consists of multiple subcommands to interact with release values.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newValuesMergeCmd(cfg, out),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
"helm.sh/helm/v4/pkg/cli/output"
|
||||
"helm.sh/helm/v4/pkg/cmd/require"
|
||||
)
|
||||
|
||||
var valuesMergeHelp = `
|
||||
This command intelligently merges values from multiple release revisions into a single
|
||||
values file, helping to solve the "version hell" problem when migrating applications
|
||||
that have undergone many releases, updates, and rollbacks.
|
||||
|
||||
It supports different merge strategies:
|
||||
- latest: Later values override earlier ones (default)
|
||||
- first: Earlier values take precedence
|
||||
- merge: Deep merge with intelligent conflict resolution
|
||||
|
||||
Examples:
|
||||
# Merge all deployed revisions of a release
|
||||
helm values merge my-release
|
||||
|
||||
# Merge specific revisions
|
||||
helm values merge my-release --revisions 1,3,5
|
||||
|
||||
# Merge a range of revisions
|
||||
helm values merge my-release --revisions 1..5
|
||||
|
||||
# Use first-wins strategy instead of latest-wins
|
||||
helm values merge my-release --strategy first
|
||||
|
||||
# Output in JSON format
|
||||
helm values merge my-release --output json
|
||||
`
|
||||
|
||||
type mergeValuesWriter struct {
|
||||
vals map[string]interface{}
|
||||
}
|
||||
|
||||
func newValuesMergeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
var outfmt output.Format
|
||||
var revisions string
|
||||
var strategy string
|
||||
var allRevisions bool
|
||||
|
||||
client := action.NewMergeValues(cfg)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "merge RELEASE_NAME",
|
||||
Short: "intelligently merge values from multiple release revisions",
|
||||
Long: valuesMergeHelp,
|
||||
Args: require.ExactArgs(1),
|
||||
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return noMoreArgsComp()
|
||||
}
|
||||
return compListReleases(toComplete, args, cfg)
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
// Parse revisions if specified
|
||||
if revisions != "" {
|
||||
parsedRevisions, err := action.ParseRevisions(revisions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid revisions specification: %w", err)
|
||||
}
|
||||
client.Revisions = parsedRevisions
|
||||
}
|
||||
|
||||
client.AllRevisions = allRevisions
|
||||
client.MergeStrategy = strategy
|
||||
client.OutputFormat = string(outfmt)
|
||||
|
||||
mergedVals, err := client.Run(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove merge metadata if user doesn't want it
|
||||
if helmMeta, ok := mergedVals["helm"].(map[string]interface{}); ok {
|
||||
if mergeMeta, ok := helmMeta["mergeMetadata"].(map[string]interface{}); ok {
|
||||
fmt.Fprintf(out, "# Merged values for release: %s\n", args[0])
|
||||
if revisionsInfo, ok := mergeMeta["revisionInfo"].([]interface{}); ok {
|
||||
fmt.Fprintf(out, "# Revisions merged: %d\n", len(revisionsInfo))
|
||||
fmt.Fprintf(out, "# Merge strategy: %s\n", strategy)
|
||||
for _, revInfo := range revisionsInfo {
|
||||
fmt.Fprintf(out, "# %s\n", revInfo)
|
||||
}
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
// Remove metadata from output
|
||||
delete(helmMeta, "mergeMetadata")
|
||||
if len(helmMeta) == 0 {
|
||||
delete(mergedVals, "helm")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove internal merge sources if present
|
||||
delete(mergedVals, "_mergeSources")
|
||||
|
||||
return outfmt.Write(out, &mergeValuesWriter{mergedVals})
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&revisions, "revisions", "", "comma-separated list of revisions to merge (e.g., '1,3,5' or '1..5')")
|
||||
f.StringVar(&strategy, "strategy", "latest", "merge strategy: latest, first, or merge (default: latest)")
|
||||
f.BoolVar(&allRevisions, "all", false, "merge all revisions of the release")
|
||||
bindOutputFlag(cmd, &outfmt)
|
||||
|
||||
// Register completion for the revisions flag
|
||||
err := cmd.RegisterFlagCompletionFunc("revisions", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 1 {
|
||||
return compListRevisions(toComplete, cfg, args[0])
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Register completion for the strategy flag
|
||||
err = cmd.RegisterFlagCompletionFunc("strategy", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
strategies := []string{"latest\tLater values override earlier ones (default)",
|
||||
"first\tEarlier values take precedence",
|
||||
"merge\tDeep merge with intelligent conflict resolution"}
|
||||
var completions []string
|
||||
for _, s := range strategies {
|
||||
if strings.HasPrefix(s, toComplete) {
|
||||
completions = append(completions, s)
|
||||
}
|
||||
}
|
||||
return completions, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m mergeValuesWriter) WriteTable(out io.Writer) error {
|
||||
fmt.Fprintln(out, "MERGED VALUES:")
|
||||
return output.EncodeYAML(out, m.vals)
|
||||
}
|
||||
|
||||
func (m mergeValuesWriter) WriteJSON(out io.Writer) error {
|
||||
return output.EncodeJSON(out, m.vals)
|
||||
}
|
||||
|
||||
func (m mergeValuesWriter) WriteYAML(out io.Writer) error {
|
||||
return output.EncodeYAML(out, m.vals)
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
"helm.sh/helm/v4/pkg/chart"
|
||||
"helm.sh/helm/v4/pkg/release"
|
||||
"helm.sh/helm/v4/pkg/storage"
|
||||
"helm.sh/helm/v4/pkg/storage/driver"
|
||||
)
|
||||
|
||||
func TestValuesMergeCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
flags map[string]string
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "requires release name",
|
||||
args: []string{},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid merge command",
|
||||
args: []string{"test-release"},
|
||||
flags: map[string]string{
|
||||
"revisions": "1,2",
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid revisions",
|
||||
args: []string{"test-release"},
|
||||
flags: map[string]string{
|
||||
"revisions": "invalid",
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test action configuration
|
||||
store := storage.Init(driver.NewMemory())
|
||||
actionConfig := action.NewConfiguration()
|
||||
actionConfig.Releases = store
|
||||
|
||||
// Create test releases
|
||||
release1 := createTestReleaseForCmd("test-release", 1)
|
||||
release2 := createTestReleaseForCmd("test-release", 2)
|
||||
|
||||
// Add releases to storage
|
||||
err := actionConfig.Releases.Create(release1)
|
||||
assert.NoError(t, err)
|
||||
err = actionConfig.Releases.Create(release2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create command
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newValuesMergeCmd(actionConfig, buf)
|
||||
|
||||
// Set flags
|
||||
for flag, value := range tt.flags {
|
||||
cmd.Flags().Set(flag, value)
|
||||
}
|
||||
|
||||
// Run command
|
||||
err = cmd.RunE(cmd, tt.args)
|
||||
|
||||
if tt.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValuesMergeHelp(t *testing.T) {
|
||||
store := storage.Init(driver.NewMemory())
|
||||
actionConfig := action.NewConfiguration()
|
||||
actionConfig.Releases = store
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newValuesMergeCmd(actionConfig, buf)
|
||||
|
||||
assert.Equal(t, "merge", cmd.Name())
|
||||
assert.Equal(t, "intelligently merge values from multiple release revisions", cmd.Short)
|
||||
assert.Contains(t, cmd.Long, "version hell")
|
||||
assert.Contains(t, cmd.Long, "merge strategies")
|
||||
}
|
||||
|
||||
func createTestReleaseForCmd(name string, version int) *release.Release {
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: name,
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
return &release.Release{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Config: map[string]interface{}{
|
||||
"replicas": version,
|
||||
"image": "nginx:1.0",
|
||||
},
|
||||
Chart: chrt,
|
||||
Manifest: "# Manifest",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
LastDeployed: "2024-01-01T00:00:00Z",
|
||||
Description: "Release description",
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue