constraints

This commit is contained in:
Pat Gavlin 2020-11-30 13:55:04 -08:00
parent bbfd8256de
commit 2818c9888d
4 changed files with 237 additions and 28 deletions

View file

@ -1,6 +1,14 @@
package deploy
import "github.com/pulumi/pulumi/sdk/v2/go/common/resource"
import (
"fmt"
"sort"
"strings"
"github.com/pulumi/pulumi/pkg/v2/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
)
// A ResourcePlan represents the planned goal state and resource operations for a single resource. The operations are
// ordered.
@ -9,21 +17,183 @@ type ResourcePlan struct {
Ops []StepOp
}
// Partial returns true if the plan is partial (i.e. its inputs properties contain unknown values).
func (rp *ResourcePlan) Partial() bool {
return rp.Goal.Properties.ContainsUnknowns()
func (rp *ResourcePlan) diffURNs(a, b []resource.URN) (message string, changed bool) {
stringsA := make([]string, len(a))
for i, urn := range a {
stringsA[i] = string(urn)
}
stringsB := make([]string, len(a))
for i, urn := range b {
stringsB[i] = string(urn)
}
return rp.diffStrings(stringsA, stringsB)
}
func (rp *ResourcePlan) completeInputs(programInputs resource.PropertyMap) resource.PropertyMap {
// Find all unknown properties and replace them with their resolved values.
plannedObject := resource.NewObjectProperty(rp.Goal.Properties.DeepCopy())
programObject := resource.NewObjectProperty(programInputs)
for _, path := range plannedObject.FindUnknowns() {
if v, ok := path.Get(programObject); ok {
path.Set(plannedObject, v)
} else {
path.Delete(plannedObject)
func (rp *ResourcePlan) diffPropertyKeys(a, b []resource.PropertyKey) (message string, changed bool) {
stringsA := make([]string, len(a))
for i, key := range a {
stringsA[i] = string(key)
}
stringsB := make([]string, len(a))
for i, key := range b {
stringsB[i] = string(key)
}
return rp.diffStrings(stringsA, stringsB)
}
func (rp *ResourcePlan) diffStrings(a, b []string) (message string, changed bool) {
setA := map[string]struct{}{}
for _, s := range a {
setA[s] = struct{}{}
}
setB := map[string]struct{}{}
for _, s := range b {
setB[s] = struct{}{}
}
var adds, deletes []string
for s := range setA {
if _, has := setB[s]; !has {
deletes = append(deletes, s)
}
}
return plannedObject.ObjectValue()
for s := range setB {
if _, has := setA[s]; !has {
adds = append(adds, s)
}
}
sort.Strings(adds)
sort.Strings(deletes)
if len(adds) == 0 && len(deletes) == 0 {
return "", false
}
if len(adds) != 0 {
message = fmt.Sprintf("added %v", strings.Join(adds, ", "))
}
if len(deletes) != 0 {
if len(adds) != 0 {
message += "; "
}
message += fmt.Sprintf("deleted %v", strings.Join(deletes, ", "))
}
return message, true
}
func (rp *ResourcePlan) diffPropertyDependencies(a, b map[resource.PropertyKey][]resource.URN) error {
return nil
}
func (rp *ResourcePlan) checkGoal(programGoal *resource.Goal) error {
contract.Assert(rp.Goal.Type == programGoal.Type)
contract.Assert(rp.Goal.Name == programGoal.Name)
// Check that either both resources are custom resources or both are component resources.
if programGoal.Custom != rp.Goal.Custom {
// TODO(pdg-plan): wording?
expected := "custom"
if !rp.Goal.Custom {
expected = "component"
}
return fmt.Errorf("resource kind changed (expected %v)", expected)
}
// Check that the provider is identical.
if rp.Goal.Provider != programGoal.Provider {
// Provider references are a combination of URN and ID, the latter of which may be unknown. Check for that
// case here.
expected, err := providers.ParseReference(rp.Goal.Provider)
if err != nil {
return fmt.Errorf("failed to parse provider reference %v: %w", rp.Goal.Provider, err)
}
actual, err := providers.ParseReference(programGoal.Provider)
if err != nil {
return fmt.Errorf("failed to parse provider reference %v: %w", programGoal.Provider, err)
}
if expected.URN() != actual.URN() || expected.ID() != providers.UnknownID {
return fmt.Errorf("provider changed (expected %v)", rp.Goal.Provider)
}
}
// Check that the parent is identical.
if programGoal.Parent != rp.Goal.Parent {
return fmt.Errorf("parent changed (expected %v)", rp.Goal.Parent)
}
// Check that the protect bit is identical.
if programGoal.Protect != rp.Goal.Protect {
return fmt.Errorf("protect changed (expected %v)", rp.Goal.Protect)
}
// Check that the DBR bit is identical.
switch {
case rp.Goal.DeleteBeforeReplace == nil && programGoal.DeleteBeforeReplace == nil:
// OK
case rp.Goal.DeleteBeforeReplace != nil && programGoal.DeleteBeforeReplace != nil:
if *rp.Goal.DeleteBeforeReplace != *programGoal.DeleteBeforeReplace {
return fmt.Errorf("deleteBeforeReplace changed (expected %v)", *rp.Goal.DeleteBeforeReplace)
}
default:
expected := "no value"
if rp.Goal.DeleteBeforeReplace != nil {
expected = fmt.Sprintf("%v", *rp.Goal.DeleteBeforeReplace)
}
return fmt.Errorf("deleteBeforeReplace changed (expected %v)", expected)
}
// Check that the import ID is identical.
if rp.Goal.ID != programGoal.ID {
return fmt.Errorf("importID changed (expected %v)", rp.Goal.ID)
}
// Check that the timeouts are identical.
switch {
case rp.Goal.CustomTimeouts.Create != programGoal.CustomTimeouts.Create:
return fmt.Errorf("create timeout changed (expected %v)", rp.Goal.CustomTimeouts.Create)
case rp.Goal.CustomTimeouts.Update != programGoal.CustomTimeouts.Update:
return fmt.Errorf("update timeout changed (expected %v)", rp.Goal.CustomTimeouts.Update)
case rp.Goal.CustomTimeouts.Delete != programGoal.CustomTimeouts.Delete:
return fmt.Errorf("delete timeout changed (expected %v)", rp.Goal.CustomTimeouts.Delete)
}
// Check that the ignoreChanges sets are identical.
if message, changed := rp.diffStrings(rp.Goal.IgnoreChanges, programGoal.IgnoreChanges); changed {
return fmt.Errorf("ignoreChanges changed: %v", message)
}
// Check that the additionalSecretOutputs sets are identical.
if message, changed := rp.diffPropertyKeys(rp.Goal.AdditionalSecretOutputs, programGoal.AdditionalSecretOutputs); changed {
return fmt.Errorf("additionalSecretOutputs changed: %v", message)
}
// Check that the alias sets are identical.
if message, changed := rp.diffURNs(rp.Goal.Aliases, programGoal.Aliases); changed {
return fmt.Errorf("aliases changed: %v", message)
}
// Check that the dependencies match.
if message, changed := rp.diffURNs(rp.Goal.Dependencies, programGoal.Dependencies); changed {
return fmt.Errorf("dependencies changed: %v", message)
}
// Check that the properties meet the constraints set in the plan.
if _, constrained := programGoal.Properties.ConstrainedTo(rp.Goal.Properties); !constrained {
// TODO(pdg-plan): message!
return fmt.Errorf("properties changed")
}
// Check that the property dependencies match. Note that because it is legal for a property that is unknown in the
// plan to be unset in the program, we allow the omission of a property from the program's dependency set.
for k, urns := range rp.Goal.PropertyDependencies {
if programDeps, ok := programGoal.PropertyDependencies[k]; ok {
if message, changed := rp.diffURNs(urns, programDeps); changed {
return fmt.Errorf("dependencies for %v changed: %v", k, message)
}
}
}
return nil
}

View file

@ -15,6 +15,7 @@
package deploy
import (
"fmt"
"strings"
"github.com/pkg/errors"
@ -235,6 +236,17 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
}
sg.urns[urn] = true
// If there is a plan for this resource, validate that the program goal conforms to the plan.
if len(sg.plan.resourcePlans) != 0 {
resourcePlan, ok := sg.plan.resourcePlans[urn]
if !ok {
return nil, result.Errorf("resource not found in plan")
}
if err := resourcePlan.checkGoal(goal); err != nil {
return nil, result.FromError(fmt.Errorf("resource violates plan: %w", err))
}
}
// Check for an old resource so that we can figure out if this is a create, delete, etc., and/or
// to diff. We look up first by URN and then by any provided aliases. If it is found using an
// alias, record that alias so that we do not delete the aliased resource later.
@ -272,13 +284,6 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
inputs = processedInputs
}
// If there is a plan for this resource, finalize its inputs.
if resourcePlan, ok := sg.plan.resourcePlans[urn]; ok {
// should really overwrite goal info completely here
inputs = resourcePlan.completeInputs(inputs)
}
// Produce a new state object that we'll build up as operations are performed. Ultimately, this is what will
// get serialized into the checkpoint file.
new := resource.NewState(goal.Type, urn, goal.Custom, false, "", inputs, nil, goal.Parent, goal.Protect, false,

View file

@ -1,6 +1,8 @@
package apitype
import (
"encoding/json"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
)
@ -47,10 +49,24 @@ type ResourcePlanV1 struct {
Steps []OpType `json:"steps,omitempty"`
}
// VersionedDeploymentPlan is a version number plus a JSON document. The version number describes what
// version of the DeploymentPlan structure the DeploymentPlan member's JSON document can decode into.
type VersionedDeploymentPlan struct {
Version int `json:"version"`
Plan json.RawMessage `json:"plan"`
}
// DeploymentPlanV1 is the serializable version of a deployment plan.
type DeploymentPlanV1 struct {
// TODO(pdg-plan): should there be a message here?
// Manifest contains metadata about this plan.
Manifest ManifestV1 `json:"manifest" yaml:"manifest"`
// Any environment variables that were set when the plan was created. Values are encrypted.
EnvironmentVariables map[string][]byte `json:"environmentVariables,omitempty"`
// The configuration in use during the plan.
Config map[string]ConfigValue `json:"config"`
// The set of resource plans.
ResourcePlans map[resource.URN]ResourcePlanV1 `json:"resourcePlans,omitempty"`
}

View file

@ -119,8 +119,7 @@ func (diff *ArrayDiff) Len() int {
// IgnoreKeyFunc is the callback type for Diff's ignore option.
type IgnoreKeyFunc func(key PropertyKey) bool
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
func (props PropertyMap) diff(other PropertyMap, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ObjectDiff {
adds := make(PropertyMap)
deletes := make(PropertyMap)
sames := make(PropertyMap)
@ -137,7 +136,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
// First find any updates or deletes.
for k, old := range props {
if ignore(k) {
if ignore(k) || ignoreUnknowns && (old.IsComputed() || old.IsOutput()) {
continue
}
@ -156,7 +155,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
} else {
sames[k] = old
}
} else if old.HasValue() {
} else if old.HasValue() && (!ignoreUnknowns || !old.IsComputed()) {
// If there was no new property, it has been deleted.
deletes[k] = old
}
@ -185,8 +184,12 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
}
}
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
return props.diff(other, false, ignoreKeys)
}
func (v PropertyValue) diff(other PropertyValue, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ValueDiff {
if v.IsArray() && other.IsArray() {
old := v.ArrayValue()
new := other.ArrayValue()
@ -239,12 +242,17 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
}
// If we got here, either the values are primitives, or they weren't the same type; do a simple diff.
if v.DeepEquals(other) {
if v.DeepEquals(other) || ignoreUnknowns && (v.IsComputed() || v.IsOutput()) {
return nil
}
return &ValueDiff{Old: v, New: other}
}
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
return v.diff(other, false, ignoreKeys)
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
// If any in props either doesn't exist, or is of a different value, return false.
@ -373,3 +381,13 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
// For all other cases, primitives are equal if their values are equal.
return v.V == other.V
}
func (m PropertyMap) ConstrainedTo(constraints PropertyMap) (*ObjectDiff, bool) {
diff := constraints.diff(m, true, nil)
return diff, diff == nil
}
func (v PropertyValue) ConstrainedTo(constraint PropertyValue) (*ValueDiff, bool) {
diff := constraint.diff(v, true, nil)
return diff, diff == nil
}