Skip to content

Commit

Permalink
add tests that highlight known issues in the destroy mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcervante committed Sep 16, 2024
1 parent 598648b commit e448e0c
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 13 deletions.
46 changes: 46 additions & 0 deletions internal/stacks/stackruntime/apply_destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestApplyDestroy(t *testing.T) {
description string
state *stackstate.State
store *stacks_testing_provider.ResourceStore
mutators []func(*stacks_testing_provider.ResourceStore, TestContext) TestContext
cycles []TestCycle
}{
"inputs-and-outputs": {
Expand Down Expand Up @@ -975,6 +976,46 @@ func TestApplyDestroy(t *testing.T) {
},
},
},
"destroy-with-provider-req": {
path: "auth-provider-w-data",
mutators: []func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext{
func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext {
store.Set("credentials", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("credentials"),
"value": cty.StringVal("zero"),
}))
testContext.providers[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) {
provider := stacks_testing_provider.NewProviderWithData(t, store)
provider.Authentication = "zero"
return provider, nil
}
return testContext
},
func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext {
store.Set("credentials", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("credentials"),
"value": cty.StringVal("one"),
}))
testContext.providers[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) {
provider := stacks_testing_provider.NewProviderWithData(t, store)
provider.Authentication = "one" // So we must reload the data source in order to authenticate.
return provider, nil
}
return testContext
},
},
cycles: []TestCycle{
{
planMode: plans.NormalMode,
},
{
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
// TODO: Populate these with the correct values.
},
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -1006,6 +1047,11 @@ func TestApplyDestroy(t *testing.T) {

state := tc.state
for ix, cycle := range tc.cycles {

if tc.mutators != nil {
testContext = tc.mutators[ix](store, testContext)
}

t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) {
var plan *stackplan.Plan
t.Run("plan", func(t *testing.T) {
Expand Down
36 changes: 35 additions & 1 deletion internal/stacks/stackruntime/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,38 @@ func TestApply(t *testing.T) {
},
},
},
"removed block with provider-to-component dep": {
path: path.Join("auth-provider-w-data", "removed"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.load")).
AddDependent(mustAbsComponent("component.create")).
AddOutputValue("credentials", cty.StringVal("wrong"))). // must reload the credentials
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.create")).
AddDependency(mustAbsComponent("component.load"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.create.testing_resource.resource")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "resource",
"value": nil,
}),
Status: states.ObjectReady,
}).
SetProviderAddr(mustDefaultRootProvider("testing"))).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().AddResource("credentials", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("credentials"),
// we have the wrong value in state, so this correct value must
// be loaded for this test to work.
"value": cty.StringVal("authn"),
})).Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
wantAppliedChanges: []stackstate.AppliedChange{},
},
},
},
}

for name, tc := range tcs {
Expand All @@ -297,7 +329,9 @@ func TestApply(t *testing.T) {
config: loadMainBundleConfigForTest(t, tc.path),
providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
provider := stacks_testing_provider.NewProviderWithData(t, store)
provider.Authentication = "authn"
return provider, nil
},
},
dependencyLocks: *lock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
}
}

// TODO: Remove from here if we want to implement the
// workaround.

// We're also going to look through any upstream components
// that are being removed to make sure they are removed first.
for _, depAddr := range c.PlanPrevDependents(ctx).Elems() {
Expand Down
3 changes: 3 additions & 0 deletions internal/stacks/stackruntime/internal/stackeval/main_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ func ApplyPlan(ctx context.Context, config *stackconfig.Config, plan *stackplan.
// dependencies to finish applying their changes.
waitForComponents = dependencyAddrs

// TODO: Remove from here if we want to implement
// the workaround.

// If we're not being destroyed we might have some
// depdendents that are being destroyed, and we need
// to wait for them to finish before we can start.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ func (p *ProviderInstance) CheckClient(ctx context.Context, phase EvalPhase) (pr
// We unmark the config before making the RPC call, as marks cannot
// be serialized.
unmarkedArgs, _ := p.ProviderArgs(ctx, phase).UnmarkDeep()
if unmarkedArgs == cty.NilVal {
// Then we had an error previously, so we'll rely on that error
// being exposed elsewhere.
return stubs.ErroredProvider(), diags
}

resp := client.ConfigureProvider(providers.ConfigureProviderRequest{
TerraformVersion: version.SemVer.String(),
Config: unmarkedArgs,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}

provider "testing" "main" {}

provider "testing" "credentialed" {
config {
authentication = component.load.credentials
}
}

component "load" {
source = "./load"

providers = {
testing = provider.testing.main
}
}

component "create" {
source = "./create"

providers = {
testing = provider.testing.credentialed
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "testing_resource" "resource" {
id = "resource"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
data "testing_data_source" "credentials" {
id = "credentials"
}

output "credentials" {
value = data.testing_data_source.credentials.value
sensitive = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}

provider "testing" "main" {}

provider "testing" "credentialed" {
config {
authentication = component.load.credentials
}
}

component "load" {
source = "../load"

providers = {
testing = provider.testing.main
}
}

removed {
source = "../create"
from = component.create
providers = {
testing = provider.testing.credentialed
}
}
65 changes: 53 additions & 12 deletions internal/stacks/stackruntime/testing/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ type MockProvider struct {
*testing_provider.MockProvider

ResourceStore *ResourceStore

// If set, authentication means the configuration must provide a value
// that matches the value here otherwise the Configure function will
// fail.
Authentication string
}

// NewProvider returns a new MockProvider with an empty data store.
Expand All @@ -89,10 +94,26 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
Provider: providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
// if the provider sets the authentication attribute,
// then it must match the internal Authentication
// value for the provider.
"authentication": {
Type: cty.String,
Sensitive: true,
Optional: true,
},

// If this value is provider, the Configure
// function call will fail and return the value
// here as part of the error.
"configure_error": {
Type: cty.String,
Optional: true,
},

// ignored allows the configuration to create
// dependencies from this provider to component
// blocks and inputs without affecting behaviour.
"ignored": {
Type: cty.String,
Optional: true,
Expand Down Expand Up @@ -131,18 +152,6 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
MoveResourceState: true,
},
},
ConfigureProviderFn: func(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
// If configure_error is set, return an error.
err := request.Config.GetAttr("configure_error")
if err.IsKnown() && !err.IsNull() {
return providers.ConfigureProviderResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.AttributeValue(tfdiags.Error, err.AsString(), "configure_error attribute was set", cty.GetAttrPath("configure_error")),
},
}
}
return providers.ConfigureProviderResponse{}
},
PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return getResource(request.TypeName).Plan(request, store)
},
Expand Down Expand Up @@ -228,6 +237,10 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
ResourceStore: store,
}

// We want to use internal fields in this function so we have to set it
// like this.
provider.ConfigureProviderFn = provider.configure

t.Cleanup(func() {
// Fail the test if this provider is not closed.
if !provider.CloseCalled {
Expand All @@ -239,6 +252,34 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
return provider
}

func (provider *MockProvider) configure(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
// If configure_error is set, return an error.
err := request.Config.GetAttr("configure_error")
if err.IsKnown() && !err.IsNull() {
return providers.ConfigureProviderResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.AttributeValue(tfdiags.Error, err.AsString(), "configure_error attribute was set", cty.GetAttrPath("configure_error")),
},
}
}

authn := request.Config.GetAttr("authentication")
if !authn.IsNull() && authn.IsKnown() {
// We deliberately only check the authentication if the configuration
// is providing it. It's entirely up to the config to opt into the
// authentication which would be crazy for a real provider but just
// makes things so much simpler for us in testing world.
if authn.AsString() != provider.Authentication {
return providers.ConfigureProviderResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field did not match expected", cty.GetAttrPath("authentication")),
},
}
}
}
return providers.ConfigureProviderResponse{}
}

// mustGenerateUUID is a helper to generate a UUID and panic if it fails.
func mustGenerateUUID() string {
val, err := uuid.GenerateUUID()
Expand Down

0 comments on commit e448e0c

Please sign in to comment.