mirror of https://github.com/pulumi/pulumi.git
466 lines
16 KiB
Go
466 lines
16 KiB
Go
// Copyright 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 fuzzing
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/stretchr/testify/require"
|
|
"pgregory.net/rapid"
|
|
)
|
|
|
|
// A ProgramSpec specifies a Pulumi program whose execution will be mocked in a lifecycle test in order to register
|
|
// resources.
|
|
type ProgramSpec struct {
|
|
// The set of resource registrations made by the program.
|
|
ResourceRegistrations []*ResourceSpec
|
|
|
|
// The set of resources present in the snapshot the program will execute against that will *not* be registered (that
|
|
// is, they will be "dropped").
|
|
Drops []*ResourceSpec
|
|
}
|
|
|
|
// Returns a new LanguageRuntimeFactory that will register the resources specified in this ProgramSpec when executed.
|
|
func (ps *ProgramSpec) AsLanguageRuntimeF(t require.TestingT) deploytest.LanguageRuntimeFactory {
|
|
return deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
// We want to emulate an actual lifecycle test as much as possible here. Consequently, rather than "hardcoding" URNs
|
|
// from the ResourceSpecs, we'll map those URNs to the actual RegisterResourceResponses we get from the resource
|
|
// monitor and use those to refer to resources in subsequent registrations. This is particularly important in the
|
|
// presence of targeted operations. Consider the following ResourceSpec for a resource named res2:
|
|
//
|
|
// ResourceSpec{
|
|
// Type: "pkgA:modA:typeA",
|
|
// Name: "res2",
|
|
// Parent: "urn:pulumi:stack::project::pkgA:modA:typeA::res1",
|
|
// AliasURNs: []resource.URN{"urn:pulumi:stack::project::pkgA:modA:typeA::res2"},
|
|
// }
|
|
//
|
|
// res2 has been updated to add a parent, res1, which previously it did not have. Its new URN,
|
|
// urn:pulumi:stack::project::pkgA:modA:typeA$pkgA:modA:typeA::res2, will include res1's type. Its old URN,
|
|
// urn:pulumi:stack::project::pkgA:modA:typeA::res2, with just res2's type, has been added as an alias. When we
|
|
// register res2, we will receive a different URN back based on whether we target it or not:
|
|
//
|
|
// * If we target it, we'll get the new URN, since the alias will find the old state, which will then be updated to
|
|
// add the parent.
|
|
//
|
|
// * If we don't target it, we'll get the old URN, since the alias will find the old state and we'll emit a SameStep
|
|
// with that state.
|
|
//
|
|
// Hardcoding the URN from the ResourceSpec would cause us to generate bad programs in the second case.
|
|
actuals := map[resource.URN]*deploytest.RegisterResourceResponse{}
|
|
rewriteProviderRef := func(oldRef string) string {
|
|
if oldRef == "" {
|
|
return ""
|
|
}
|
|
|
|
parsed, err := providers.ParseReference(oldRef)
|
|
require.NoError(t, err)
|
|
|
|
res, hasRes := actuals[parsed.URN()]
|
|
if !hasRes {
|
|
return oldRef
|
|
}
|
|
|
|
newRef, err := providers.NewReference(res.URN, res.ID)
|
|
require.NoError(t, err)
|
|
|
|
return newRef.String()
|
|
}
|
|
|
|
rewriteURN := func(oldURN resource.URN) resource.URN {
|
|
if res, hasRes := actuals[oldURN]; hasRes {
|
|
return res.URN
|
|
}
|
|
|
|
return oldURN
|
|
}
|
|
|
|
for _, r := range ps.ResourceRegistrations {
|
|
opts := deploytest.ResourceOptions{
|
|
Protect: r.Protect,
|
|
RetainOnDelete: r.RetainOnDelete,
|
|
Parent: rewriteURN(r.Parent),
|
|
Provider: rewriteProviderRef(r.Provider),
|
|
DeletedWith: rewriteURN(r.DeletedWith),
|
|
|
|
// We explicitly *don't* want to rewrite aliases since they are not dependencies and refer to (we expect)
|
|
// resources in the state, not the program we are running.
|
|
AliasURNs: r.Aliases,
|
|
}
|
|
|
|
deps := make([]resource.URN, len(r.Dependencies))
|
|
for i, dep := range r.Dependencies {
|
|
deps[i] = rewriteURN(dep)
|
|
}
|
|
|
|
propDeps := map[resource.PropertyKey][]resource.URN{}
|
|
for k, deps := range r.PropertyDependencies {
|
|
propDeps[k] = make([]resource.URN, len(deps))
|
|
for i, dep := range deps {
|
|
propDeps[k][i] = rewriteURN(dep)
|
|
}
|
|
}
|
|
|
|
opts.Dependencies = deps
|
|
opts.PropertyDeps = propDeps
|
|
|
|
res, err := monitor.RegisterResource(r.Type, r.Name, r.Custom, opts)
|
|
require.NoError(t, err)
|
|
|
|
actuals[r.URN()] = res
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Implements PrettySpec.Pretty. Returns a human-readable representation of this ProgramSpec, suitable for use in
|
|
// debugging output and error messages.
|
|
func (ps *ProgramSpec) Pretty(indent string) string {
|
|
rendered := fmt.Sprintf("%sProgram %p", indent, ps)
|
|
|
|
if len(ps.ResourceRegistrations) == 0 {
|
|
rendered += fmt.Sprintf("\n%s No registrations", indent)
|
|
} else {
|
|
rendered += fmt.Sprintf("\n%s Registrations (%d):", indent, len(ps.ResourceRegistrations))
|
|
for _, r := range ps.ResourceRegistrations {
|
|
rendered += fmt.Sprintf("\n%s %s", indent, r.Pretty(indent+" "))
|
|
}
|
|
}
|
|
|
|
if len(ps.Drops) == 0 {
|
|
rendered += fmt.Sprintf("\n%s No drops", indent)
|
|
} else {
|
|
rendered += fmt.Sprintf("\n%s Drops (%d):", indent, len(ps.Drops))
|
|
for _, r := range ps.Drops {
|
|
rendered += fmt.Sprintf("\n%s %s", indent, r.Pretty(indent+" "))
|
|
}
|
|
}
|
|
|
|
return rendered
|
|
}
|
|
|
|
// The type of tags that may be added to resources in a ProgramSpec.
|
|
type ProgramResourceTag string
|
|
|
|
const (
|
|
// Tags a resource as having been newly prepended to the program.
|
|
NewlyPrependedProgramResource ProgramResourceTag = "program.newly-prepended"
|
|
|
|
// Tags a resource as having been dropped from the program.
|
|
DroppedProgramResource ProgramResourceTag = "program.dropped"
|
|
|
|
// Tags a resource as having been newly inserted into the program (that is, between two resources that already exist
|
|
// in the snapshot the program will execute against).
|
|
NewlyInsertedProgramResource ProgramResourceTag = "program.newly-inserted"
|
|
|
|
// Tags a resource as having been updated in the program (that is, provided a new set of inputs to those held in the
|
|
// snapshot that the program will execute against).
|
|
UpdatedProgramResource ProgramResourceTag = "program.updated"
|
|
|
|
// Tags a resource as having been copied from the snapshot the program will execute against.
|
|
CopiedProgramResource ProgramResourceTag = "program.copied"
|
|
|
|
// Tags a resource as having been newly appended to the program.
|
|
NewlyAppendedProgramResource ProgramResourceTag = "program.newly-appended"
|
|
)
|
|
|
|
// A set of options for configuring the generation of a ProgramSpec.
|
|
type ProgramSpecOptions struct {
|
|
PrependCount *rapid.Generator[int]
|
|
PrependResourceOpts ResourceSpecOptions
|
|
Action *rapid.Generator[ProgramSpecAction]
|
|
InsertResourceOpts ResourceSpecOptions
|
|
UpdateProtect *rapid.Generator[bool]
|
|
UpdateRetainOnDelete *rapid.Generator[bool]
|
|
AddAliases *rapid.Generator[bool]
|
|
AppendCount *rapid.Generator[int]
|
|
AppendResourceOpts ResourceSpecOptions
|
|
}
|
|
|
|
// The set of actions that may be taken on resources in a ProgramSpec.
|
|
type ProgramSpecAction string
|
|
|
|
const (
|
|
// Deletes a resource from the program.
|
|
ProgramSpecDelete ProgramSpecAction = "delete"
|
|
// Inserts a new resource into the program.
|
|
ProgramSpecInsert ProgramSpecAction = "insert"
|
|
// Updates a resource in the program.
|
|
ProgramSpecUpdate ProgramSpecAction = "update"
|
|
// Copies a resource from the snapshot the program will execute against.
|
|
ProgramSpecCopy ProgramSpecAction = "copy"
|
|
)
|
|
|
|
// Returns a copy of the given ProgramSpecOptions with the given overrides applied.
|
|
func (pso ProgramSpecOptions) With(overrides ProgramSpecOptions) ProgramSpecOptions {
|
|
if overrides.PrependCount != nil {
|
|
pso.PrependCount = overrides.PrependCount
|
|
}
|
|
pso.PrependResourceOpts = pso.PrependResourceOpts.With(overrides.PrependResourceOpts)
|
|
if overrides.Action != nil {
|
|
pso.Action = overrides.Action
|
|
}
|
|
pso.InsertResourceOpts = pso.InsertResourceOpts.With(overrides.InsertResourceOpts)
|
|
if overrides.UpdateProtect != nil {
|
|
pso.UpdateProtect = overrides.UpdateProtect
|
|
}
|
|
if overrides.UpdateRetainOnDelete != nil {
|
|
pso.UpdateRetainOnDelete = overrides.UpdateRetainOnDelete
|
|
}
|
|
if overrides.AddAliases != nil {
|
|
pso.AddAliases = overrides.AddAliases
|
|
}
|
|
if overrides.AppendCount != nil {
|
|
pso.AppendCount = overrides.AppendCount
|
|
}
|
|
pso.AppendResourceOpts = pso.AppendResourceOpts.With(overrides.AppendResourceOpts)
|
|
|
|
return pso
|
|
}
|
|
|
|
// A default set of ProgramSpecOptions. By default, we'll prepend and append between 0 and 2 resources, and take actions
|
|
// on existing resources with equal probability.
|
|
var defaultProgramSpecOptions = ProgramSpecOptions{
|
|
PrependCount: rapid.IntRange(0, 2),
|
|
PrependResourceOpts: defaultResourceSpecOptions,
|
|
Action: rapid.SampledFrom(programSpecActions),
|
|
InsertResourceOpts: defaultResourceSpecOptions,
|
|
UpdateProtect: rapid.Bool(),
|
|
UpdateRetainOnDelete: rapid.Bool(),
|
|
AddAliases: rapid.Bool(),
|
|
AppendCount: rapid.IntRange(0, 2),
|
|
AppendResourceOpts: defaultResourceSpecOptions,
|
|
}
|
|
|
|
var programSpecActions = []ProgramSpecAction{
|
|
ProgramSpecDelete,
|
|
ProgramSpecInsert,
|
|
ProgramSpecUpdate,
|
|
ProgramSpecCopy,
|
|
}
|
|
|
|
// Given a SnapshotSpec and a set of options, returns a rapid.Generator that will produce ProgramSpecs that operate upon
|
|
// the specified snapshot.
|
|
func GeneratedProgramSpec(
|
|
ss *SnapshotSpec,
|
|
sso StackSpecOptions,
|
|
pso ProgramSpecOptions,
|
|
) *rapid.Generator[*ProgramSpec] {
|
|
sso = defaultStackSpecOptions.With(sso)
|
|
pso = defaultProgramSpecOptions.With(pso)
|
|
|
|
return rapid.Custom(func(t *rapid.T) *ProgramSpec {
|
|
drops := []*ResourceSpec{}
|
|
|
|
// Whenever we copy a resource from the snapshot, we need to ensure that its dependencies have been updated to take
|
|
// deletions/drops into account. The dropped and rewritten maps and the updateDependencies function take care of
|
|
// this.
|
|
//
|
|
// While in many cases updating is just a case of removing references to dropped resources, we may also need to
|
|
// *rewrite* references whenever parent/child relationships change. This is because a resource's URN changes
|
|
// whenever its parent changes.
|
|
//
|
|
// When a URN changes due to parent/child changes, we'll consult the AddAliases generator to decide whether to drop
|
|
// the old URN, or add an alias to the new resource pointing to it. Based on this, subsequent resources that refer
|
|
// to the old URN will either have their references pruned or updated to the new URN.
|
|
|
|
dropped := map[resource.URN]bool{}
|
|
rewritten := map[resource.URN]resource.URN{}
|
|
|
|
updateDependencies := func(r *ResourceSpec) {
|
|
deps := []resource.URN{}
|
|
propDeps := map[resource.PropertyKey][]resource.URN{}
|
|
|
|
// We'll start with parents first. If our parent was dropped, we'll need to remove them from our parent reference.
|
|
// If they were rewritten, we'll update the reference to point to the new URN.
|
|
oldURN := r.URN()
|
|
if dropped[r.Parent] {
|
|
r.Parent = ""
|
|
} else if newParent, hasNewParent := rewritten[r.Parent]; hasNewParent {
|
|
r.Parent = newParent
|
|
}
|
|
|
|
// If our URN changed (e.g. because our parent changed), we'll consult the AddAliases generator to decide whether
|
|
// to drop the old URN or add an alias to the new resource pointing to it.
|
|
if oldURN != r.URN() {
|
|
shouldAlias := pso.AddAliases.Draw(t, "ProgramSpec.PrunedResource.AddAliases")
|
|
if shouldAlias {
|
|
r.Aliases = []resource.URN{oldURN}
|
|
rewritten[oldURN] = r.URN()
|
|
} else {
|
|
dropped[oldURN] = true
|
|
}
|
|
}
|
|
|
|
// Updating dependencies, property dependencies and deleted-with relationships is simpler, since these don't
|
|
// affect our URN.
|
|
for _, dep := range r.Dependencies {
|
|
if dropped[dep] {
|
|
continue
|
|
} else if newDep, hasNewDep := rewritten[dep]; hasNewDep {
|
|
deps = append(deps, newDep)
|
|
} else {
|
|
deps = append(deps, dep)
|
|
}
|
|
}
|
|
|
|
for k, deps := range r.PropertyDependencies {
|
|
for _, dep := range deps {
|
|
if dropped[dep] {
|
|
continue
|
|
} else if newDep, hasNewDep := rewritten[dep]; hasNewDep {
|
|
propDeps[k] = append(propDeps[k], newDep)
|
|
} else {
|
|
propDeps[k] = append(propDeps[k], dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
if dropped[r.DeletedWith] {
|
|
r.DeletedWith = ""
|
|
} else if newDep, hasNewDep := rewritten[r.DeletedWith]; hasNewDep {
|
|
r.DeletedWith = newDep
|
|
}
|
|
|
|
r.Dependencies = deps
|
|
r.PropertyDependencies = propDeps
|
|
}
|
|
|
|
newSS := &SnapshotSpec{}
|
|
|
|
prependCount := pso.PrependCount.Draw(t, "ProgramSpec.PrependCount")
|
|
if prependCount > 0 {
|
|
// If we're going to prepend resources, we need to ensure that we have a provider to use for them.
|
|
initialProvider := generatedProviderResourceSpec(newSS, sso).Draw(t, "SnapshotSpec.InitialProvider")
|
|
AddTag(initialProvider, "program.provider.initial")
|
|
newSS.AddProvider(initialProvider)
|
|
}
|
|
|
|
for i := 0; i < prependCount; i++ {
|
|
r := generatedNewResourceSpec(
|
|
newSS,
|
|
sso,
|
|
pso.PrependResourceOpts,
|
|
).Draw(t, fmt.Sprintf("ProgramSpec.PrependedResource[%d]", i))
|
|
AddTag(r, NewlyPrependedProgramResource)
|
|
newSS.AddResource(r)
|
|
}
|
|
|
|
for i := 0; i < len(ss.Resources); {
|
|
if ss.Resources[i].Delete {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
action := pso.Action.Draw(t, fmt.Sprintf("ProgramSpec.Action[%d]", i))
|
|
if action == ProgramSpecDelete {
|
|
r := ss.Resources[i].Copy()
|
|
if providers.IsProviderType(r.Type) {
|
|
AddTag(r, CopiedProgramResource)
|
|
updateDependencies(r)
|
|
|
|
newSS.AddResource(r)
|
|
} else {
|
|
AddTag(r, DroppedProgramResource)
|
|
|
|
drops = append(drops, r)
|
|
dropped[r.URN()] = true
|
|
}
|
|
|
|
i++
|
|
} else if action == ProgramSpecInsert && len(newSS.Providers) > 0 {
|
|
r := generatedNewResourceSpec(
|
|
newSS,
|
|
sso,
|
|
pso.InsertResourceOpts,
|
|
).Draw(t, fmt.Sprintf("ProgramSpec.InsertedResource[%d]", i))
|
|
AddTag(r, NewlyInsertedProgramResource)
|
|
newSS.AddResource(r)
|
|
} else if action == ProgramSpecUpdate {
|
|
r := ss.Resources[i].Copy()
|
|
AddTag(r, UpdatedProgramResource)
|
|
|
|
// We'll generate a new set of dependencies for the updated resource, which means we'll automatically take any
|
|
// drops or deletions into account.
|
|
rds := GeneratedResourceDependencies(newSS, r, includeAll).
|
|
Draw(t, fmt.Sprintf("ProgramSpec.UpdatedResource[%d].Dependencies", i))
|
|
|
|
if pso.UpdateProtect != nil {
|
|
r.Protect = pso.UpdateProtect.Draw(t, fmt.Sprintf("ProgramSpec.UpdatedResource[%d].Protect", i))
|
|
}
|
|
if pso.UpdateRetainOnDelete != nil {
|
|
r.RetainOnDelete = pso.UpdateRetainOnDelete.Draw(
|
|
t,
|
|
fmt.Sprintf("ProgramSpec.UpdatedResource[%d].RetainOnDelete", i),
|
|
)
|
|
}
|
|
|
|
oldURN := r.URN()
|
|
rds.ApplyTo(r)
|
|
if oldURN != r.URN() {
|
|
shouldAlias := pso.AddAliases.Draw(t, fmt.Sprintf("ProgramSpec.UpdatedResource[%d].AddAliases", i))
|
|
if shouldAlias {
|
|
r.Aliases = []resource.URN{oldURN}
|
|
rewritten[oldURN] = r.URN()
|
|
} else {
|
|
dropped[oldURN] = true
|
|
}
|
|
}
|
|
|
|
newSS.AddResource(r)
|
|
i++
|
|
} else {
|
|
r := ss.Resources[i].Copy()
|
|
AddTag(r, CopiedProgramResource)
|
|
updateDependencies(r)
|
|
|
|
newSS.AddResource(r)
|
|
i++
|
|
}
|
|
}
|
|
|
|
appendCount := pso.AppendCount.Draw(t, "ProgramSpec.AppendCount")
|
|
if appendCount > 0 && len(newSS.Providers) == 0 {
|
|
// If we're going to append resources and the snapshot is still empty, we need to ensure that we have a provider
|
|
// to use for them.
|
|
initialProvider := generatedProviderResourceSpec(newSS, sso).Draw(t, "SnapshotSpec.InitialProvider")
|
|
AddTag(initialProvider, "program.provider.initial")
|
|
newSS.AddProvider(initialProvider)
|
|
}
|
|
|
|
for i := 0; i < appendCount; i++ {
|
|
r := generatedNewResourceSpec(
|
|
newSS,
|
|
sso,
|
|
pso.AppendResourceOpts,
|
|
).Draw(t, fmt.Sprintf("ProgramSpec.AppendedResource[%d]", i))
|
|
AddTag(r, NewlyAppendedProgramResource)
|
|
newSS.AddResource(r)
|
|
}
|
|
|
|
ps := &ProgramSpec{
|
|
ResourceRegistrations: newSS.Resources,
|
|
Drops: drops,
|
|
}
|
|
|
|
return ps
|
|
})
|
|
}
|