diff --git a/pkg/secrets/strategy.go b/pkg/secrets/strategy.go index 498328a48..c475a9d3e 100644 --- a/pkg/secrets/strategy.go +++ b/pkg/secrets/strategy.go @@ -13,20 +13,6 @@ import ( // This is the output of resolving a parameter or credential set file. type Set map[string]string -// Merge merges a second Set into the base. -// -// Duplicate names are not allow and will result in an -// error, this is the case even if the values are identical. -func (s Set) Merge(s2 Set) error { - for k, v := range s2 { - if _, ok := s[k]; ok { - return fmt.Errorf("ambiguous value resolution: %q is already present in base sets, cannot merge", k) - } - s[k] = v - } - return nil -} - // IsValid determines if the provided key (designating a name of a parameter // or credential) is included in the provided set func (s Set) IsValid(key string) bool { @@ -130,18 +116,84 @@ func (s Source) MarshalYAML() (interface{}, error) { return s.MarshalRaw(), nil } -type StrategyList []SourceMap +// SourceMapList is a list of mappings that can be access via index or the item name. +type SourceMapList []SourceMap -func (l StrategyList) Less(i, j int) bool { +func (l SourceMapList) Less(i, j int) bool { return l[i].Name < l[j].Name } -func (l StrategyList) Swap(i, j int) { +func (l SourceMapList) Swap(i, j int) { tmp := l[i] l[i] = l[j] l[j] = tmp } -func (l StrategyList) Len() int { +func (l SourceMapList) Len() int { return len(l) } + +// HasName determines if the specified name is defined in the set. +func (l SourceMapList) HasName(name string) bool { + _, ok := l.GetByName(name) + return ok +} + +// GetByName returns the resolution strategy for the specified name and a bool indicating if it was found. +func (l SourceMapList) GetByName(name string) (SourceMap, bool) { + for _, item := range l { + if item.Name == name { + return item, true + } + } + + return SourceMap{}, false +} + +// GetResolvedValue returns the resolved value of the specified name and a bool indicating if it was found. +// You must resolve the value before calling, it does not do resolution for you. +func (l SourceMapList) GetResolvedValue(name string) (interface{}, bool) { + item, ok := l.GetByName(name) + if ok { + return item.ResolvedValue, true + } + + return nil, false +} + +// ToResolvedValues converts the items to a map of key/value pairs, with the resolved values represented as CNAB-compatible strings +func (l SourceMapList) ToResolvedValues() map[string]string { + values := make(map[string]string, len(l)) + for _, item := range l { + values[item.Name] = item.ResolvedValue + } + return values +} + +// Merge applies the specified values on top of a base set of values. When a +// name exists in both sets, use the value from the overrides +func (l SourceMapList) Merge(overrides SourceMapList) SourceMapList { + result := l + + // Make a lookup from the name to its index in result so that we can quickly find a + // named item while merging + lookup := make(map[string]int, len(result)) + for i, item := range result { + lookup[item.Name] = i + } + + for _, item := range overrides { + // If the name is in the base, overwrite its value with the override provided + if i, ok := lookup[item.Name]; ok { + result[i].Source = item.Source + + // Just in case the value was resolved, include in the merge results + result[i].ResolvedValue = item.ResolvedValue + } else { + // Append the override to the list of results if it's not in the base set of values + result = append(result, item) + } + } + + return result +} diff --git a/pkg/secrets/strategy_test.go b/pkg/secrets/strategy_test.go index 8e5478fe9..da00d7f66 100644 --- a/pkg/secrets/strategy_test.go +++ b/pkg/secrets/strategy_test.go @@ -1,30 +1,105 @@ package secrets import ( + "sort" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestSet_Merge(t *testing.T) { - set := Set{ - "first": "first", - "second": "second", - "third": "third", +func TestSourceMapList_GetResolvedValue(t *testing.T) { + l := SourceMapList{ + SourceMap{Name: "bar", ResolvedValue: "2"}, + SourceMap{Name: "foo", ResolvedValue: "1"}, + } + + fooVal, ok := l.GetResolvedValue("foo") + require.True(t, ok, "foo could not be found") + assert.Equal(t, "1", fooVal, "foo had the incorrect resolved value") + + _, ok = l.GetResolvedValue("missing") + require.False(t, ok, "GetResolvedValue should have returned that missing key was not found") +} + +func TestSourceMapList_GetResolvedValues(t *testing.T) { + l := SourceMapList{ + SourceMap{Name: "bar", ResolvedValue: "2"}, + SourceMap{Name: "foo", ResolvedValue: "1"}, + } + + want := map[string]string{ + "bar": "2", + "foo": "1", + } + got := l.ToResolvedValues() + assert.Equal(t, want, got) +} + +func TestSourceMapList_HasKey(t *testing.T) { + l := SourceMapList{ + SourceMap{Name: "bar", ResolvedValue: "2"}, + SourceMap{Name: "foo", ResolvedValue: "1"}, + } + + require.True(t, l.HasName("foo"), "HasName returned the wrong value for a key present in the list") + require.False(t, l.HasName("missing"), "HasName returned the wrong value for a missing key") +} + +func TestSourceMapList_Sort(t *testing.T) { + l := SourceMapList{ + SourceMap{Name: "c"}, + SourceMap{Name: "a"}, + SourceMap{Name: "b"}, + } + + sort.Sort(l) + + wantResult := SourceMapList{ + SourceMap{Name: "a"}, + SourceMap{Name: "b"}, + SourceMap{Name: "c"}, + } + + require.Len(t, l, 3, "Len is not implemented correctly") + require.Equal(t, wantResult, l, "Sort is not implemented correctly") +} + +func TestSourceMapList_GetStrategy(t *testing.T) { + wantToken := SourceMap{Name: "token", Source: Source{Strategy: "env", Hint: "GITHUB_TOKEN"}} + l := SourceMapList{ + SourceMap{Name: "logLevel", Source: Source{Strategy: "value", Hint: "11"}}, + wantToken, + } + + gotToken, ok := l.GetByName("token") + require.True(t, ok, "GetByName did not find 'token'") + assert.Equal(t, wantToken, gotToken, "GetByName returned the wrong 'token' strategy") +} + +func TestSourceMapList_Merge(t *testing.T) { + set := SourceMapList{ + SourceMap{Name: "first", ResolvedValue: "base first"}, + SourceMap{Name: "second", ResolvedValue: "base second"}, + SourceMap{Name: "third", ResolvedValue: "base third"}, } is := assert.New(t) - err := set.Merge(Set{}) - is.NoError(err) - is.Len(set, 3) - is.NotContains(set, "fourth") + result := set.Merge(SourceMapList{}) + is.Len(result, 3) + _, ok := result.GetResolvedValue("base fourth") + is.False(ok, "forth should not be present in the base set") - err = set.Merge(Set{"fourth": "fourth"}) - is.NoError(err) - is.Len(set, 4) - is.Contains(set, "fourth") + result = set.Merge(SourceMapList{SourceMap{Name: "fourth", ResolvedValue: "new fourth"}}) + is.Len(result, 4) + val, ok := result.GetResolvedValue("fourth") + is.True(ok, "fourth should be present when merged as an override") + is.Equal("new fourth", val, "incorrect merged value") - err = set.Merge(Set{"second": "bis"}) - is.EqualError(err, `ambiguous value resolution: "second" is already present in base sets, cannot merge`) + result = set.Merge(SourceMapList{SourceMap{Name: "second", ResolvedValue: "override second"}}) + is.Len(result, 3) + val, ok = result.GetResolvedValue("second") + is.True(ok, "second should be overwritten when an override value is merged") + is.Equal("override second", val, "incorrect merged value") } diff --git a/pkg/storage/credentialset.go b/pkg/storage/credentialset.go index 651fd5252..07bc0577a 100644 --- a/pkg/storage/credentialset.go +++ b/pkg/storage/credentialset.go @@ -50,7 +50,7 @@ type CredentialSetSpec struct { Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" toml:"labels,omitempty"` // Credentials is a list of credential resolution strategies. - Credentials secrets.StrategyList `json:"credentials" yaml:"credentials" toml:"credentials"` + Credentials secrets.SourceMapList `json:"credentials" yaml:"credentials" toml:"credentials"` } // CredentialSetStatus contains additional status metadata that has been set by Porter. diff --git a/pkg/storage/parameterset.go b/pkg/storage/parameterset.go index 66f104c8c..c31ffeecf 100644 --- a/pkg/storage/parameterset.go +++ b/pkg/storage/parameterset.go @@ -44,7 +44,7 @@ type ParameterSetSpec struct { Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" toml:"labels,omitempty"` // Parameters is a list of parameter specs. - Parameters secrets.StrategyList `json:"parameters" yaml:"parameters" toml:"parameters"` + Parameters secrets.SourceMapList `json:"parameters" yaml:"parameters" toml:"parameters"` } // ParameterSetStatus contains additional status metadata that has been set by Porter.