Skip to content

Commit

Permalink
Fix automatic aliasing of muxed providers (#1938)
Browse files Browse the repository at this point in the history
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
t0yv0 committed May 15, 2024
1 parent 38a6882 commit 1c1c073
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 92 deletions.
53 changes: 4 additions & 49 deletions pf/internal/muxer/muxer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package muxer

import (
"fmt"
"sort"
"strings"

"context"
Expand All @@ -26,7 +25,6 @@ import (
"github.com/pulumi/pulumi-terraform-bridge/pf/internal/schemashim"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
shimSchema "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema"
"github.com/pulumi/pulumi-terraform-bridge/x/muxer"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
Expand Down Expand Up @@ -124,10 +122,10 @@ func (m *ProviderShim) DataSourceIsPF(token string) bool {
//
// `provider` will be the `len(m.MuxedProviders)` when mappings are computed.
func (m *ProviderShim) extend(provider shim.Provider) ([]string, []string) {
res, conflictingResources := union(m.resources, provider.ResourcesMap())

data, conflictingDataSources := union(m.dataSources, provider.DataSourcesMap())

res := newUnionMap(m.resources, provider.ResourcesMap())
conflictingResources := res.ConflictingKeys()
data := newUnionMap(m.dataSources, provider.DataSourcesMap())
conflictingDataSources := data.ConflictingKeys()
m.resources = res
m.dataSources = data
m.MuxedProviders = append(m.MuxedProviders, provider)
Expand Down Expand Up @@ -255,46 +253,3 @@ func (p *simpleSchemaProvider) DataSourcesMap() shim.ResourceMap {
}

var _ shim.Provider = (*simpleSchemaProvider)(nil)

func union(baseline, extension shim.ResourceMap) (shim.ResourceMap, []string) {
union, conflictingKeys := mapUnion(toResourceMap(baseline), toResourceMap(extension))
return shimSchema.ResourceMap(union), conflictingKeys
}

func toResourceMap(rmap shim.ResourceMap) shimSchema.ResourceMap {
m := map[string]shim.Resource{}
rmap.Range(func(key string, value shim.Resource) bool {
m[key] = value
return true
})
return m
}

func mapUnion[T any](baseline, extension map[string]T) (map[string]T, []string) {
u := copyMap(baseline)

var conflictingKeys []string

for k, v := range extension {
if _, conflict := baseline[k]; conflict {
conflictingKeys = append(conflictingKeys, k)
continue
}
u[k] = v
}

sort.Strings(conflictingKeys)

return u, conflictingKeys
}

func copyMap[K comparable, V any](m map[K]V) map[K]V {
if m == nil {
return nil
}
out := make(map[K]V, len(m))
for k, v := range m {
out[k] = v
}
return out
}
143 changes: 143 additions & 0 deletions pf/internal/muxer/union.go
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
}
141 changes: 141 additions & 0 deletions pf/internal/muxer/union_test.go
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
}
}
}
11 changes: 4 additions & 7 deletions pf/internal/schemashim/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

type SchemaOnlyProvider struct {
ctx context.Context
tf pfprovider.Provider
ctx context.Context
tf pfprovider.Provider
resourceMap shim.ResourceMap
}

func (p *SchemaOnlyProvider) PfProvider() pfprovider.Provider {
Expand All @@ -45,11 +46,7 @@ func (p *SchemaOnlyProvider) Schema() shim.SchemaMap {
}

func (p *SchemaOnlyProvider) ResourcesMap() shim.ResourceMap {
resources, err := pfutils.GatherResources(context.TODO(), p.tf)
if err != nil {
panic(err)
}
return &schemaOnlyResourceMap{resources}
return p.resourceMap
}

func (p *SchemaOnlyProvider) DataSourcesMap() shim.ResourceMap {
Expand Down
Loading

0 comments on commit 1c1c073

Please sign in to comment.