-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix automatic aliasing of muxed providers (#1938)
Fixes #1937 This PR fixes the automatic aliasing of muxed providers by dispatching a ResourceMap Clone operation to the sub-map that owns the key being cloned and making PF resource maps mutable.
- Loading branch information
Showing
9 changed files
with
396 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright 2016-2024, Pulumi Corporation. | ||
// | ||
// 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 muxer | ||
|
||
import ( | ||
"fmt" | ||
"sort" | ||
|
||
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" | ||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" | ||
) | ||
|
||
type unionMap[T any] struct { | ||
baseline mapLike[T] | ||
extension mapLike[T] | ||
} | ||
|
||
type mapLike[T any] interface { | ||
Len() int | ||
Range(func(key string, value T) bool) | ||
GetOk(key string) (T, bool) | ||
Set(key string, value T) | ||
} | ||
|
||
var _ shim.ResourceMapWithClone = (*unionMap[shim.Resource])(nil) | ||
|
||
func newUnionMap[T any](baseline, extension mapLike[T]) *unionMap[T] { | ||
return &unionMap[T]{ | ||
baseline: baseline, | ||
extension: extension, | ||
} | ||
} | ||
|
||
func (m *unionMap[T]) Len() int { | ||
n := m.baseline.Len() | ||
m.extension.Range(func(key string, value T) bool { | ||
if _, conflict := m.baseline.GetOk(key); !conflict { | ||
n++ | ||
} | ||
return true | ||
}) | ||
return n | ||
} | ||
|
||
func (m *unionMap[T]) Get(key string) T { | ||
if v, ok := m.GetOk(key); ok { | ||
return v | ||
} | ||
contract.Failf("key not found: %v", key) | ||
var zero T | ||
return zero | ||
} | ||
|
||
func (m *unionMap[T]) GetOk(key string) (T, bool) { | ||
if v, ok := m.baseline.GetOk(key); ok { | ||
return v, true | ||
} | ||
if v, ok := m.extension.GetOk(key); ok { | ||
return v, true | ||
} | ||
var zero T | ||
return zero, false | ||
} | ||
|
||
func (m *unionMap[T]) Range(each func(key string, value T) bool) { | ||
iterating := true | ||
m.baseline.Range(func(key string, value T) bool { | ||
iterating = iterating && each(key, value) | ||
return iterating | ||
}) | ||
if !iterating { | ||
return | ||
} | ||
m.extension.Range(func(key string, value T) bool { | ||
if _, conflict := m.baseline.GetOk(key); conflict { | ||
return true | ||
} | ||
return each(key, value) | ||
}) | ||
} | ||
|
||
func (m *unionMap[T]) Set(key string, value T) { | ||
// Sending edits to the owner map. | ||
_, b := m.baseline.GetOk(key) | ||
_, e := m.extension.GetOk(key) | ||
switch { | ||
case b && e: | ||
contract.Failf("Cannot set a conflicting key in a union of two maps: %q", key) | ||
case b: | ||
m.baseline.Set(key, value) | ||
case e: | ||
m.extension.Set(key, value) | ||
default: | ||
// Net-new keys accumulate in baseline, this is arbitrary: | ||
m.baseline.Set(key, value) | ||
} | ||
} | ||
|
||
// Clone will delegate the operation to the sub-map owning oldKey, or fail if the owner is ambiguous. In the sub-map the | ||
// value associated with oldKey will now be also associated with newKey. In Pulumi this is relied on to track ownership | ||
// of aliased resources in muxed providers as RenameResourceWithAlias uses Clone. | ||
func (m *unionMap[T]) Clone(oldKey, newKey string) error { | ||
// Sending the clone operation to the owner map. | ||
bv, b := m.baseline.GetOk(oldKey) | ||
ev, e := m.extension.GetOk(oldKey) | ||
|
||
switch { | ||
case b && e: | ||
return fmt.Errorf("Cannot clone a conflicting key in a union of two maps: %q", oldKey) | ||
case b: | ||
m.baseline.Set(newKey, bv) | ||
return nil | ||
case e: | ||
m.extension.Set(newKey, ev) | ||
return nil | ||
default: | ||
return fmt.Errorf("Cannot clone a non-existing key %q to %q", oldKey, newKey) | ||
} | ||
} | ||
|
||
func (m *unionMap[T]) ConflictingKeys() []string { | ||
conflicts := []string{} | ||
m.baseline.Range(func(key string, value T) bool { | ||
if _, ok := m.extension.GetOk(key); ok { | ||
conflicts = append(conflicts, key) | ||
} | ||
return true | ||
}) | ||
sort.Strings(conflicts) | ||
return conflicts | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
// Copyright 2016-2024, Pulumi Corporation. | ||
// | ||
// 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 muxer | ||
|
||
import ( | ||
"sort" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestUnionMap(t *testing.T) { | ||
m1 := testMap{ | ||
"one": "n1", | ||
"two": "n2", | ||
} | ||
m2 := testMap{ | ||
"two": "x2", | ||
"three": "x3", | ||
} | ||
m := newUnionMap(m1, m2) | ||
|
||
t.Run("Len", func(t *testing.T) { | ||
assert.Equal(t, 3, m.Len()) | ||
}) | ||
t.Run("GetOk", func(t *testing.T) { | ||
_, ok := m.GetOk("zero") | ||
assert.False(t, ok) | ||
n1, ok := m.GetOk("one") | ||
assert.Equal(t, "n1", n1) | ||
assert.True(t, ok) | ||
// Conflicting keys served from the left (baseline) map. | ||
n2, ok := m.GetOk("two") | ||
assert.Equal(t, "n2", n2) | ||
assert.True(t, ok) | ||
n3, ok := m.GetOk("three") | ||
assert.Equal(t, "x3", n3) | ||
assert.True(t, ok) | ||
}) | ||
t.Run("Get", func(t *testing.T) { | ||
n1 := m.Get("one") | ||
assert.Equal(t, "n1", n1) | ||
}) | ||
t.Run("ConflictingKeys", func(t *testing.T) { | ||
assert.Equal(t, []string{"two"}, m.ConflictingKeys()) | ||
}) | ||
t.Run("Range", func(t *testing.T) { | ||
keys := []string{} | ||
values := []string{} | ||
m.Range(func(key, value string) bool { | ||
keys = append(keys, key) | ||
values = append(values, value) | ||
return true | ||
}) | ||
sort.Strings(keys) | ||
sort.Strings(values) | ||
assert.Equal(t, []string{"one", "three", "two"}, keys) | ||
assert.Equal(t, []string{"n1", "n2", "x3"}, values) | ||
}) | ||
t.Run("Set", func(t *testing.T) { | ||
m1 := testMap{ | ||
"one": "n1", | ||
"two": "n2", | ||
} | ||
m2 := testMap{ | ||
"two": "x2", | ||
"three": "x3", | ||
} | ||
m := newUnionMap(m1, m2) | ||
m.Set("one", "n1!") | ||
assert.Equal(t, "n1!", m1["one"]) | ||
m.Set("three", "x3!") | ||
assert.Equal(t, "x3!", m2["three"]) | ||
m.Set("zero", "n0!") | ||
assert.Equal(t, "n0!", m1["zero"]) | ||
t.Run("conflict", func(t *testing.T) { | ||
defer func() { | ||
if r := recover(); r == nil { | ||
t.Errorf("The code did not panic") | ||
} | ||
}() | ||
m.Set("two", "n2!") | ||
}) | ||
}) | ||
t.Run("Clone", func(t *testing.T) { | ||
m1 := testMap{ | ||
"one": "n1", | ||
"two": "n2", | ||
} | ||
m2 := testMap{ | ||
"two": "x2", | ||
"three": "x3", | ||
} | ||
m := newUnionMap(m1, m2) | ||
err := m.Clone("one", "one_legacy") | ||
assert.NoError(t, err) | ||
assert.Equal(t, "n1", m1["one_legacy"]) | ||
err = m.Clone("three", "three_legacy") | ||
assert.NoError(t, err) | ||
assert.Equal(t, "x3", m2["three_legacy"]) | ||
assert.Error(t, m.Clone("two", "two_legacy")) | ||
assert.Error(t, m.Clone("zero", "zero_legacy")) | ||
}) | ||
} | ||
|
||
type testMap map[string]string | ||
|
||
var _ mapLike[string] = make(testMap) | ||
|
||
func (x testMap) Len() int { | ||
return len(x) | ||
} | ||
|
||
func (x testMap) GetOk(key string) (string, bool) { | ||
v, ok := x[key] | ||
return v, ok | ||
} | ||
|
||
func (x testMap) Set(key string, value string) { | ||
x[key] = value | ||
} | ||
|
||
func (x testMap) Range(each func(key string, value string) bool) { | ||
for k, v := range x { | ||
if !each(k, v) { | ||
return | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.