Respect provider aliases (#7166)

This commit is contained in:
Evan Boyle 2021-07-28 12:12:53 -07:00 committed by GitHub
parent 5b2fdb27d3
commit f4efb7564b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 1 deletions

View file

@ -1,3 +1,6 @@
### Improvements
### Bug Fixes
- [cli] - Respect provider aliases
[#7166](https://github.com/pulumi/pulumi/pull/7166)

View file

@ -41,6 +41,9 @@ func Destroy(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
}
defer emitter.Close()
logging.V(7).Infof("*** Starting Destroy(preview=%v) ***", dryRun)
defer logging.V(7).Infof("*** Destroy(preview=%v) complete ***", dryRun)
return update(ctx, info, deploymentOptions{
UpdateOptions: opts,
SourceFunc: newDestroySource,

View file

@ -2,10 +2,13 @@
package lifecycletest
import (
"sync"
"testing"
"github.com/blang/semver"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
@ -338,6 +341,279 @@ func TestSingleResourceExplicitProviderReplace(t *testing.T) {
p.Run(t, snap)
}
type configurableProvider struct {
id string
replace bool
creates *sync.Map
deletes *sync.Map
}
func (p *configurableProvider) configure(news resource.PropertyMap) error {
p.id = news["id"].StringValue()
return nil
}
func (p *configurableProvider) create(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
uid, err := uuid.NewV4()
if err != nil {
return "", nil, resource.StatusUnknown, err
}
id := resource.ID(uid.String())
p.creates.Store(id, p.id)
return id, inputs, resource.StatusOK, nil
}
func (p *configurableProvider) delete(urn resource.URN, id resource.ID, olds resource.PropertyMap,
timeout float64) (resource.Status, error) {
p.deletes.Store(id, p.id)
return resource.StatusOK, nil
}
// TestSingleResourceExplicitProviderAliasUpdateDelete verifies that providers respect aliases during updates, and
// that the correct instance of an explicit provider is used to delete a removed resource.
func TestSingleResourceExplicitProviderAliasUpdateDelete(t *testing.T) {
var creates, deletes sync.Map
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
configurable := &configurableProvider{
creates: &creates,
deletes: &deletes,
}
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{}, nil
},
ConfigureF: configurable.configure,
CreateF: configurable.create,
DeleteF: configurable.delete,
}, nil
}),
}
providerInputs := resource.PropertyMap{
resource.PropertyKey("id"): resource.NewStringProperty("first"),
}
providerName := "provA"
aliases := []resource.URN{}
registerResource := true
var resourceID resource.ID
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), providerName, true,
deploytest.ResourceOptions{
Inputs: providerInputs,
Aliases: aliases,
})
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
if registerResource {
_, resourceID, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Provider: provRef.String(),
})
assert.NoError(t, err)
}
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its initial update+refresh.
p.Steps = steps[:4]
snap := p.Run(t, nil)
// Add a provider alias to the original URN.
aliases = []resource.URN{
p.NewProviderURN("pkgA", "provA", ""),
}
// Change the provider name and configuration and remove the resource. This will cause an Update for the provider
// and a Delete for the resource. The updated provider instance should be used to perform the delete.
providerName = "provB"
providerInputs[resource.PropertyKey("id")] = resource.NewStringProperty("second")
registerResource = false
p.Steps = []TestStep{{Op: Update}}
_ = p.Run(t, snap)
// Check the identity of the provider that performed the delete.
deleterID, ok := deletes.Load(resourceID)
require.True(t, ok)
assert.Equal(t, "second", deleterID)
}
// TestSingleResourceExplicitProviderAliasReplace verifies that providers respect aliases,
// and propagate replaces as a result of an aliased provider diff.
func TestSingleResourceExplicitProviderAliasReplace(t *testing.T) {
var creates, deletes sync.Map
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
configurable := &configurableProvider{
replace: true,
creates: &creates,
deletes: &deletes,
}
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{ReplaceKeys: keys}, nil
},
ConfigureF: configurable.configure,
CreateF: configurable.create,
DeleteF: configurable.delete,
}, nil
}),
}
providerInputs := resource.PropertyMap{
resource.PropertyKey("id"): resource.NewStringProperty("first"),
}
providerName := "provA"
aliases := []resource.URN{}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), providerName, true,
deploytest.ResourceOptions{
Inputs: providerInputs,
Aliases: aliases,
})
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Provider: provRef.String(),
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its no-op update+refresh.
p.Steps = steps[:4]
snap := p.Run(t, nil)
// add a provider alias to the original URN
aliases = []resource.URN{
p.NewProviderURN("pkgA", "provA", ""),
}
// change the provider name
providerName = "provB"
// run an update expecting no-op respecting the aliases.
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
for _, entry := range entries {
if entry.Step.Op() != deploy.OpSame {
t.Fatalf("update should contain no changes: %v", entry.Step.URN())
}
}
return res
},
}}
snap = p.Run(t, snap)
// Change the config and run an update maintaining the alias. We expect everything to require replacement.
providerInputs[resource.PropertyKey("id")] = resource.NewStringProperty("second")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
provURN := p.NewProviderURN("pkgA", providerName, "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Find the delete and create IDs for the resource.
var createdID, deletedID resource.ID
// Look for replace steps on the provider and the resource.
replacedProvider, replacedResource := false, false
for _, entry := range entries {
op := entry.Step.Op()
if entry.Step.URN() == resURN {
switch op {
case deploy.OpCreateReplacement:
createdID = entry.Step.New().ID
case deploy.OpDeleteReplaced:
deletedID = entry.Step.Old().ID
}
}
if entry.Kind != JournalEntrySuccess || op != deploy.OpDeleteReplaced {
continue
}
switch urn := entry.Step.URN(); urn {
case provURN:
replacedProvider = true
case resURN:
replacedResource = true
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.True(t, replacedProvider)
assert.True(t, replacedResource)
// Check the identities of the providers that performed the create and delete.
//
// For a replacement, the newly-created provider should be used to create the new resource, and the original
// provider should be used to delete the old resource.
creatorID, ok := creates.Load(createdID)
require.True(t, ok)
assert.Equal(t, "second", creatorID)
deleterID, ok := deletes.Load(deletedID)
require.True(t, ok)
assert.Equal(t, "first", deleterID)
return res
},
}}
snap = p.Run(t, snap)
// Resume the lifecycle with another no-op update.
p.Steps = steps[2:]
p.Run(t, snap)
}
func TestSingleResourceExplicitProviderDeleteBeforeReplace(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {

View file

@ -44,6 +44,9 @@ func Refresh(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
// Force opts.Refresh to true.
opts.Refresh = true
logging.V(7).Infof("*** Starting Refresh(preview=%v) ***", dryRun)
defer logging.V(7).Infof("*** Refresh(preview=%v) complete ***", dryRun)
return update(ctx, info, deploymentOptions{
UpdateOptions: opts,
SourceFunc: newRefreshSource,

View file

@ -179,6 +179,9 @@ func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resour
}
defer emitter.Close()
logging.V(7).Infof("*** Starting Update(preview=%v) ***", dryRun)
defer logging.V(7).Infof("*** Update(preview=%v) complete ***", dryRun)
return update(ctx, info, deploymentOptions{
UpdateOptions: opts,
SourceFunc: newUpdateSource,

View file

@ -360,6 +360,10 @@ func (d *Deployment) Prev() *Snapshot { return d.prev }
func (d *Deployment) Olds() map[resource.URN]*resource.State { return d.olds }
func (d *Deployment) Source() Source { return d.source }
func (d *Deployment) SameProvider(ref providers.Reference) {
d.providers.Same(ref)
}
func (d *Deployment) GetProvider(ref providers.Reference) (plugin.Provider, bool) {
return d.providers.GetProvider(ref)
}

View file

@ -65,6 +65,7 @@ type Registry struct {
isPreview bool
providers map[Reference]plugin.Provider
builtins plugin.Provider
aliases map[resource.URN]resource.URN
m sync.RWMutex
}
@ -91,6 +92,7 @@ func NewRegistry(host plugin.Host, prev []*resource.State, isPreview bool,
isPreview: isPreview,
providers: make(map[Reference]plugin.Provider),
builtins: builtins,
aliases: make(map[resource.URN]resource.URN),
}
for _, res := range prev {
@ -156,6 +158,10 @@ func (r *Registry) setProvider(ref Reference, provider plugin.Provider) {
logging.V(7).Infof("setProvider(%v)", ref)
r.providers[ref] = provider
if alias, ok := r.aliases[ref.URN()]; ok {
r.providers[mustNewReference(alias, ref.ID())] = provider
}
}
func (r *Registry) deleteProvider(ref Reference) (plugin.Provider, bool) {
@ -254,6 +260,14 @@ func (r *Registry) Check(urn resource.URN, olds, news resource.PropertyMap,
return inputs, nil, nil
}
// RegisterAliases informs the registry that the new provider object with the given URN is aliased to the given list
// of URNs.
func (r *Registry) RegisterAlias(providerURN, alias resource.URN) {
if providerURN != alias {
r.aliases[providerURN] = alias
}
}
// Diff diffs the configuration of the indicated provider. The provider corresponding to the given URN must have
// previously been loaded by a call to Check.
func (r *Registry) Diff(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
@ -299,9 +313,27 @@ func (r *Registry) Diff(urn resource.URN, id resource.ID, olds, news resource.Pr
contract.IgnoreError(closeErr)
}
logging.V(7).Infof("%s: executed (%#v, %#v)", label, diff.Changes, diff.ReplaceKeys)
return diff, nil
}
// Same executes as part of the "Same" step for a provider that has not changed. It exists solely to allow the registry
// to point aliases for a provider to the proper object.
func (r *Registry) Same(ref Reference) {
r.m.RLock()
defer r.m.RUnlock()
logging.V(7).Infof("Same(%v)", ref)
// If this provider is aliased to a different old URN, make sure that it is present under both the old reference and
// the new reference.
if alias, ok := r.aliases[ref.URN()]; ok {
aliasRef := mustNewReference(alias, ref.ID())
r.providers[ref] = r.providers[aliasRef]
}
}
// Create coonfigures the provider with the given URN using the indicated configuration, assigns it an ID, and
// registers it under the assigned (URN, ID).
//

View file

@ -121,9 +121,22 @@ func (s *SameStep) Res() *resource.State { return s.new }
func (s *SameStep) Logical() bool { return true }
func (s *SameStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error) {
// Retain the ID, and outputs:
// Retain the ID and outputs
s.new.ID = s.old.ID
s.new.Outputs = s.old.Outputs
// If the resource is a provider, ensure that it is present in the registry under the appropriate URNs.
if providers.IsProviderType(s.new.Type) {
ref, err := providers.NewReference(s.new.URN, s.new.ID)
if err != nil {
return resource.StatusOK, nil, errors.Errorf(
"bad provider reference '%v' for resource %v: %v", s.Provider(), s.URN(), err)
}
if s.Deployment() != nil {
s.Deployment().SameProvider(ref)
}
}
complete := func() { s.reg.Done(&RegisterResult{State: s.new}) }
return resource.StatusOK, complete, nil
}

View file

@ -228,6 +228,9 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
sg.deployment.Diag().Errorf(diag.GetDuplicateResourceAliasError(urn), urnOrAlias, urn, previousAliasURN)
}
sg.aliased[urnOrAlias] = urn
// register the alias with the provider registry
sg.deployment.providers.RegisterAlias(urn, urnOrAlias)
}
break
}