pulumi/pkg/resource/deploy/providers/reference.go
Sean Gillespie 2870518a64 Refine resource replacement logic for providers (#2767)
This commit touches an intersection of a few different provider-oriented
features that combined to cause a particularly severe bug that made it
impossible for users to upgrade provider versions without seeing
replacements with their resources.

For some context, Pulumi models all providers as resources and places
them in the snapshot like any other resource. Every resource has a
reference to the provider that created it. If a Pulumi program does not
specify a particular provider to use when performing a resource
operation, the Pulumi engine injects one automatically; these are called
"default providers" and are the most common ways that users end up with
providers in their snapshot. Default providers can be identified by
their name, which is always prefixed with "default".

Recently, in an effort to make the Pulumi engine more flexible with
provider versions, it was made possible for the engine to have multiple
default providers active for a provider of a particular type, which was
previously not possible. Because a provider is identified as a tuple of
package name and version, it was difficult to find a name for these
duplicate default providers that did not cause additional problems. The
provider versioning PR gave these default providers a name that was
derived from the version of the package. This proved to be a problem,
because when users upgraded from one version of a package to another,
this changed the name of their default provider which in turn caused all
of their resources created using that provider (read: everything) to be
replaced.

To combat this, this PR introduces a rule that the engine will apply
when diffing a resource to determine whether or not it needs to be
replaced: "If a resource's provider changes, and both old and new
providers are default providers whose properties do not require
replacement, proceed as if there were no diff." This allows the engine
to gracefully recognize and recover when a resource's default provider changes
names, as long as the provider's config has not changed.
2019-06-03 12:16:31 -07:00

120 lines
4.1 KiB
Go

// Copyright 2016-2018, 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 providers
import (
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// A provider reference is (URN, ID) tuple that refers to a particular provider instance. A provider reference's
// string representation is <URN> "::" <ID>. The URN's type portion must be of the form "pulumi:providers:<pkg>".
// UnknownID is a distinguished token used to indicate that a provider's ID is not known (e.g. because we are
// performing a preview).
const UnknownID = plugin.UnknownStringValue
// IsProviderType returns true if the supplied type token refers to a Pulumi provider.
func IsProviderType(typ tokens.Type) bool {
// Tokens without a module member are definitely not provider types.
if !tokens.Token(typ).HasModuleMember() {
return false
}
return typ.Module() == "pulumi:providers" && typ.Name() != ""
}
// IsDefaultProvider returns true if this URN refers to a default Pulumi provider.
func IsDefaultProvider(urn resource.URN) bool {
return IsProviderType(urn.Type()) && strings.HasPrefix(urn.Name().String(), "default")
}
// MakeProviderType returns the provider type token for the given package.
func MakeProviderType(pkg tokens.Package) tokens.Type {
return tokens.Type("pulumi:providers:" + pkg)
}
// GetProviderPackage returns the provider package for the given type token.
func GetProviderPackage(typ tokens.Type) tokens.Package {
contract.Require(IsProviderType(typ), "typ")
return tokens.Package(typ.Name())
}
func validateURN(urn resource.URN) error {
typ := urn.Type()
if typ.Module() != "pulumi:providers" {
return errors.Errorf("invalid module in type: expected 'pulumi:providers', got '%v'", typ.Module())
}
if typ.Name() == "" {
return errors.New("provider URNs must specify a type name")
}
return nil
}
// Reference represents a reference to a particular provider.
type Reference struct {
urn resource.URN
id resource.ID
}
// URN returns the provider reference's URN.
func (r Reference) URN() resource.URN {
return r.urn
}
// ID returns the provider reference's ID.
func (r Reference) ID() resource.ID {
return r.id
}
// String returns the string representation of this provider reference.
func (r Reference) String() string {
return string(r.urn) + resource.URNNameDelimiter + string(r.id)
}
// NewReference creates a new reference for the given URN and ID.
func NewReference(urn resource.URN, id resource.ID) (Reference, error) {
if err := validateURN(urn); err != nil {
return Reference{}, err
}
return Reference{urn: urn, id: id}, nil
}
func mustNewReference(urn resource.URN, id resource.ID) Reference {
ref, err := NewReference(urn, id)
contract.Assert(err == nil)
return ref
}
// ParseReference parses the URN and ID from the string representation of a provider reference. If parsing was
// not possible, this function returns false.
func ParseReference(s string) (Reference, error) {
// If this is not a valid URN + ID, return false. Note that we don't try terribly hard to validate the URN portion
// of the reference.
lastSep := strings.LastIndex(s, resource.URNNameDelimiter)
if lastSep == -1 {
return Reference{}, errors.Errorf("expected '%v' in provider reference '%v'", resource.URNNameDelimiter, s)
}
urn, id := resource.URN(s[:lastSep]), resource.ID(s[lastSep+len(resource.URNNameDelimiter):])
if err := validateURN(urn); err != nil {
return Reference{}, err
}
return Reference{urn: urn, id: id}, nil
}