pulumi/pkg/engine/lifecycletest/fuzzing/resource.go

450 lines
15 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"
"slices"
"strings"
"github.com/mitchellh/copystructure"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"golang.org/x/exp/maps"
"pgregory.net/rapid"
)
// A ResourceSpec specifies the subset of a resource's state that is relevant to fuzzing snapshot integrity issues.
// Generally this encompasses enough to identify a resource (URN, ID, and so on) and any dependencies it may have on
// others.
type ResourceSpec struct {
Project tokens.PackageName
Stack tokens.QName
Type tokens.Type
Name string
ID resource.ID
Custom bool
Delete bool
Protect bool
PendingReplacement bool
RetainOnDelete bool
Provider string
Parent resource.URN
Dependencies []resource.URN
PropertyDependencies map[resource.PropertyKey][]resource.URN
DeletedWith resource.URN
Aliases []resource.URN
// A set of tags associated with the resource. These have no bearing on any tests but are included to aid in debugging
// and identifying the causes of snapshot integrity issues.
Tags map[string]bool
}
// Creates a ResourceSpec from the given resource.State.
func FromResource(r *resource.State) *ResourceSpec {
deps := copystructure.Must(copystructure.Copy(r.Dependencies)).([]resource.URN)
propDeps := copystructure.Must(copystructure.Copy(r.PropertyDependencies)).(map[resource.PropertyKey][]resource.URN)
aliases := copystructure.Must(copystructure.Copy(r.Aliases)).([]resource.URN)
return &ResourceSpec{
Project: r.URN.Project(),
Stack: r.URN.Stack(),
Type: r.Type,
Name: r.URN.Name(),
Custom: r.Custom,
Delete: r.Delete,
ID: r.ID,
Protect: r.Protect,
PendingReplacement: r.PendingReplacement,
RetainOnDelete: r.RetainOnDelete,
Provider: r.Provider,
Parent: r.Parent,
Dependencies: deps,
PropertyDependencies: propDeps,
DeletedWith: r.DeletedWith,
Aliases: aliases,
Tags: map[string]bool{},
}
}
// Creates a ResourceSpec from the given ResourceV3.
func FromResourceV3(r apitype.ResourceV3) *ResourceSpec {
deps := copystructure.Must(copystructure.Copy(r.Dependencies)).([]resource.URN)
propDeps := copystructure.Must(copystructure.Copy(r.PropertyDependencies)).(map[resource.PropertyKey][]resource.URN)
aliases := copystructure.Must(copystructure.Copy(r.Aliases)).([]resource.URN)
return &ResourceSpec{
Project: r.URN.Project(),
Stack: r.URN.Stack(),
Type: r.Type,
Name: r.URN.Name(),
Custom: r.Custom,
Delete: r.Delete,
ID: r.ID,
Protect: r.Protect,
PendingReplacement: r.PendingReplacement,
RetainOnDelete: r.RetainOnDelete,
Provider: r.Provider,
Parent: r.Parent,
Dependencies: deps,
PropertyDependencies: propDeps,
DeletedWith: r.DeletedWith,
Aliases: aliases,
Tags: map[string]bool{},
}
}
// AddTag adds the given tag to the given ResourceSpec. Ideally this would be a generic method on ResourceSpec itself,
// but Go doesn't support generic methods yet.
func AddTag[T ~string](r *ResourceSpec, tag T) {
if tag == "" {
return
}
if r.Tags == nil {
r.Tags = map[string]bool{}
}
r.Tags[string(tag)] = true
}
// URN returns the URN of this ResourceSpec.
func (r *ResourceSpec) URN() resource.URN {
var parentType tokens.Type
if r.Parent != "" {
parentType = r.Parent.QualifiedType()
}
return resource.NewURN(r.Stack, r.Project, parentType, r.Type, r.Name)
}
// Copy returns a deep copy of this ResourceSpec.
func (r *ResourceSpec) Copy() *ResourceSpec {
deps := copystructure.Must(copystructure.Copy(r.Dependencies)).([]resource.URN)
propDeps := copystructure.Must(copystructure.Copy(r.PropertyDependencies)).(map[resource.PropertyKey][]resource.URN)
aliases := copystructure.Must(copystructure.Copy(r.Aliases)).([]resource.URN)
tags := copystructure.Must(copystructure.Copy(r.Tags)).(map[string]bool)
return &ResourceSpec{
Project: r.Project,
Stack: r.Stack,
Type: r.Type,
Name: r.Name,
Custom: r.Custom,
Delete: r.Delete,
ID: r.ID,
Protect: r.Protect,
PendingReplacement: r.PendingReplacement,
RetainOnDelete: r.RetainOnDelete,
Parent: r.Parent,
Provider: r.Provider,
Dependencies: deps,
PropertyDependencies: propDeps,
DeletedWith: r.DeletedWith,
Aliases: aliases,
Tags: tags,
}
}
// Returns a resource.State representation of this ResourceSpec, suitable for inclusion in e.g. a snapshot.
func (r *ResourceSpec) AsResource() *resource.State {
deps := copystructure.Must(copystructure.Copy(r.Dependencies)).([]resource.URN)
propDeps := copystructure.Must(copystructure.Copy(r.PropertyDependencies)).(map[resource.PropertyKey][]resource.URN)
aliases := copystructure.Must(copystructure.Copy(r.Aliases)).([]resource.URN)
tags := maps.Keys(r.Tags)
slices.Sort(tags)
s := &resource.State{
Type: r.Type,
URN: r.URN(),
Custom: r.Custom,
Delete: r.Delete,
ID: r.ID,
Protect: r.Protect,
PendingReplacement: r.PendingReplacement,
RetainOnDelete: r.RetainOnDelete,
Provider: r.Provider,
Parent: r.Parent,
Dependencies: deps,
PropertyDependencies: propDeps,
DeletedWith: r.DeletedWith,
Aliases: aliases,
SourcePosition: strings.Join(tags, ", "),
}
// In order to allow us to control generated resource IDs (e.g. such as those returned by a provider Create call),
// we'll set the ResourceSpec's ID field as an input property.
if !providers.IsProviderType(r.Type) {
s.Inputs = resource.PropertyMap{
"__id": resource.NewStringProperty(r.ID.String()),
}
}
return s
}
// Implements PrettySpec.Pretty. Returns a human-readable representation of this ResourceSpec, suitable for use in
// debugging output and error messages.
//
// <urn> [provider, custom]
//
// Tags: program.updated, snapshot.provider.initial
//
// Protect: false
// Pending replacement: false
// Retain on delete: false
//
// Dependencies (1):
// <urn>
func (r *ResourceSpec) Pretty(indent string) string {
var b strings.Builder
b.WriteString(Colored(r.URN()))
b.WriteString(" [")
if providers.IsProviderType(r.Type) {
b.WriteString("provider, ")
}
if r.Custom {
b.WriteString("custom")
} else {
b.WriteString("component")
}
if r.Delete {
b.WriteString(", deleted")
}
b.WriteRune(']')
if len(r.Tags) > 0 {
ks := maps.Keys(r.Tags)
slices.Sort(ks)
b.WriteString(fmt.Sprintf("\n%s Tags: %s", indent, strings.Join(ks, ", ")))
}
if r.ID != "" {
b.WriteString(fmt.Sprintf("\n%s ID: %s", indent, r.ID))
}
if r.Protect {
b.WriteString(fmt.Sprintf("\n%s Protect: true", indent))
}
if r.PendingReplacement {
b.WriteString(fmt.Sprintf("\n%s Pending replacement: true", indent))
}
if r.RetainOnDelete {
b.WriteString(fmt.Sprintf("\n%s Retain on delete: true", indent))
}
if r.Provider != "" {
provRef, err := providers.ParseReference(r.Provider)
if err != nil {
b.WriteString(fmt.Sprintf("\n%s Provider: %s", indent, r.Provider))
} else {
b.WriteString(fmt.Sprintf("\n%s Provider: %s::%s", indent, Colored(provRef.URN()), provRef.ID()))
}
}
if r.Parent != "" {
b.WriteString(fmt.Sprintf("\n%s Parent: %s", indent, Colored(r.Parent)))
}
if len(r.Dependencies) > 0 {
b.WriteString(fmt.Sprintf("\n\n%s Dependencies (%d):", indent, len(r.Dependencies)))
for _, d := range r.Dependencies {
b.WriteString(fmt.Sprintf("\n%s %s", indent, Colored(d)))
}
}
if len(r.PropertyDependencies) > 0 {
b.WriteString(fmt.Sprintf("\n\n%s Property dependencies (%d key[s]):", indent, len(r.PropertyDependencies)))
for k, deps := range r.PropertyDependencies {
b.WriteString(fmt.Sprintf("\n%s %s", indent, k))
for _, d := range deps {
b.WriteString(fmt.Sprintf("\n%s %s", indent, Colored(d)))
}
}
}
if r.DeletedWith != "" {
b.WriteString(fmt.Sprintf("\n\n%s Deleted with: %s", indent, Colored(r.DeletedWith)))
}
if len(r.Aliases) > 0 {
b.WriteString(fmt.Sprintf("\n\n%s Aliases (%d):", indent, len(r.Aliases)))
for _, a := range r.Aliases {
b.WriteString(fmt.Sprintf("\n%s %s", indent, Colored(a)))
}
}
return b.String()
}
// Given a package name, returns a rapid.Generator that yields random resource types within that package.
//
// GeneratedResourceType("pkg-xyz").Draw(t, "ResourceType") = "pkg-xyz:<mod>:<type>"
func GeneratedResourceType(pkg tokens.Package) *rapid.Generator[tokens.Type] {
return rapid.Custom(func(t *rapid.T) tokens.Type {
mod := rapid.StringMatching("^mod-[a-z][A-Za-z0-9]{3}$").Draw(t, "ResourceType.Module")
typ := rapid.StringMatching("^type-[a-z][A-Za-z0-9]{3}$").Draw(t, "ResourceType.Type")
return tokens.Type(fmt.Sprintf("%s:%s:%s", pkg, mod, typ))
})
}
// A rapid.Generator that yields random provider types.
//
// GeneratedProviderType.Draw(t, "ProviderType") = "pulumi:providers:<pkg>"
var GeneratedProviderType = rapid.Custom(func(t *rapid.T) tokens.Type {
pkg := rapid.StringMatching("^pkg-[a-z][A-Za-z0-9]{3}$").Draw(t, "ProviderType.Package")
return tokens.Type("pulumi:providers:" + pkg)
})
// A rapid.Generator that yields random resource names.
//
// GeneratedResourceName.Draw(t, "ResourceName") = "res-<random>"
var GeneratedResourceName = rapid.Custom(func(t *rapid.T) string {
name := rapid.StringMatching("^res-[a-z][A-Za-z0-9]{3}$").Draw(t, "ResourceName")
return name
})
// A rapid.Generator that yields random resource IDs.
//
// GeneratedResourceID.Draw(t, "ResourceID") = "id-<random>"
var GeneratedResourceID = rapid.Custom(func(t *rapid.T) resource.ID {
id := rapid.StringMatching("^id-[a-z][A-Za-z0-9]{11}$").Draw(t, "ResourceID")
return resource.ID(id)
})
// A set of options for configuring the generation of a ResourceSpec.
type ResourceSpecOptions struct {
Custom *rapid.Generator[bool]
Protect *rapid.Generator[bool]
PendingReplacement *rapid.Generator[bool]
RetainOnDelete *rapid.Generator[bool]
}
// Returns a copy of the given ResourceSpecOptions with the given overrides applied.
func (rso ResourceSpecOptions) With(overrides ResourceSpecOptions) ResourceSpecOptions {
if overrides.Custom != nil {
rso.Custom = overrides.Custom
}
if overrides.Protect != nil {
rso.Protect = overrides.Protect
}
if overrides.PendingReplacement != nil {
rso.PendingReplacement = overrides.PendingReplacement
}
if overrides.RetainOnDelete != nil {
rso.RetainOnDelete = overrides.RetainOnDelete
}
return rso
}
// A default set of ResourceSpecOptions. By default, all configurations are equally likely.
var defaultResourceSpecOptions = ResourceSpecOptions{
Custom: rapid.Bool(),
Protect: rapid.Bool(),
PendingReplacement: rapid.Bool(),
RetainOnDelete: rapid.Bool(),
}
// Given a set of StackSpecOptions, returns a rapid.Generator that yields random provider ResourceSpecs with no
// dependencies. Provider resources are always custom and never deleted.
func GeneratedProviderResourceSpec(
sso StackSpecOptions,
) *rapid.Generator[*ResourceSpec] {
sso = defaultStackSpecOptions.With(sso)
return rapid.Custom(func(t *rapid.T) *ResourceSpec {
typ := GeneratedProviderType.Draw(t, "ProviderResourceSpec.Type")
name := GeneratedResourceName.Draw(t, "ProviderResourceSpec.Name")
id := GeneratedResourceID.Draw(t, "ProviderResourceSpec.ID")
r := &ResourceSpec{
Project: tokens.PackageName(sso.Project),
Stack: tokens.QName(sso.Stack),
Type: typ,
Name: name,
ID: id,
Custom: true,
Delete: false,
Protect: false,
PendingReplacement: false,
RetainOnDelete: false,
Dependencies: []resource.URN{},
PropertyDependencies: map[resource.PropertyKey][]resource.URN{},
Aliases: []resource.URN{},
Tags: map[string]bool{},
}
return r
})
}
// Given a set of StackSpecOptions, ResourceSpecOptions, and a map of package names to provider resources, returns a
// rapid.Generator that yields random ResourceSpecs with no dependencies.
func GeneratedResourceSpec(
sso StackSpecOptions,
rso ResourceSpecOptions,
provs map[tokens.Package]*ResourceSpec,
) *rapid.Generator[*ResourceSpec] {
sso = defaultStackSpecOptions.With(sso)
rso = defaultResourceSpecOptions.With(rso)
return rapid.Custom(func(t *rapid.T) *ResourceSpec {
pkg := rapid.SampledFrom(maps.Keys(provs)).Draw(t, "ResourceSpec.Package")
provider := provs[pkg]
typ := GeneratedResourceType(pkg).Draw(t, "ResourceSpec.Type")
name := GeneratedResourceName.Draw(t, "ResourceSpec.Name")
id := GeneratedResourceID.Draw(t, "ResourceSpec.ID")
r := &ResourceSpec{
Project: tokens.PackageName(sso.Project),
Stack: tokens.QName(sso.Stack),
Type: typ,
Name: name,
ID: id,
Custom: rso.Custom.Draw(t, "ResourceSpec.Custom"),
Delete: false,
Protect: rso.Protect.Draw(t, "ResourceSpec.Protect"),
PendingReplacement: rso.PendingReplacement.Draw(t, "ResourceSpec.PendingReplacement"),
RetainOnDelete: rso.RetainOnDelete.Draw(t, "ResourceSpec.RetainOnDelete"),
Provider: fmt.Sprintf("%s::%s", provider.URN(), provider.ID),
Dependencies: []resource.URN{},
PropertyDependencies: map[resource.PropertyKey][]resource.URN{},
Aliases: []resource.URN{},
Tags: map[string]bool{},
}
return r
})
}