stacks: test resource identity

pull/36684/head
Daniel Schmidt 1 year ago
parent 49cc9391f7
commit bc901abcc5

@ -5463,6 +5463,106 @@ func TestPlan_RemovedBlocks(t *testing.T) {
}
}
func TestPlanWithResourceIdentities(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "resource-identity")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: map[string]plans.DynamicValue{
"name": mustPlanDynamicValueDynamicType(cty.StringVal("example")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{"name": nil},
PlannedOutputValues: map[string]cty.Value{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource_with_identity.hello"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource_with_identity.hello"),
PrevRunAddr: mustAbsResourceInstance("testing_resource_with_identity.hello"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("example"),
"value": cty.NullVal(cty.String),
})),
AfterIdentity: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("id:example"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceWithIdentitySchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
// collectPlanOutput consumes the two output channels emitting results from
// a call to [Plan], and collects all of the data written to them before
// returning once changesCh has been closed by the sender to indicate that

@ -0,0 +1,7 @@
variable "name" {
type = string
}
resource "testing_resource_with_identity" "hello" {
id = var.name
}

@ -0,0 +1,19 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
provider "testing" "main" {}
component "self" {
source = "./"
inputs = {
name = "example"
}
providers = {
testing = provider.testing.main
}
}

@ -67,6 +67,21 @@ var (
},
},
}
TestingResourceWithIdentitySchema = providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"value": {Type: cty.String, Optional: true},
},
},
Identity: &configschema.Object{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
},
Nesting: configschema.NestingSingle,
},
}
)
// MockProvider wraps the standard MockProvider with a simple in-memory
@ -148,6 +163,10 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
"testing_blocked_resource": {
Body: BlockedResourceSchema.Body,
},
"testing_resource_with_identity": {
Body: TestingResourceSchema.Body,
Identity: TestingResourceWithIdentitySchema.Identity,
},
},
DataSources: map[string]providers.Schema{
"testing_data_source": {

@ -35,6 +35,8 @@ func getResource(name string) resource {
return &failedResource{}
case "testing_blocked_resource":
return &blockedResource{}
case "testing_resource_with_identity":
return &testingResourceWithIdentity{}
default:
panic("unknown resource: " + name)
}
@ -45,6 +47,7 @@ var (
_ resource = (*deferredResource)(nil)
_ resource = (*failedResource)(nil)
_ resource = (*blockedResource)(nil)
_ resource = (*testingResourceWithIdentity)(nil)
)
// testingResource is a simple resource that can be managed by the mock provider
@ -316,6 +319,66 @@ func (b *blockedResource) Apply(request providers.ApplyResourceChangeRequest, st
return
}
// testingResourceWithIdentity is the same as testingResource but it returns an identity.
type testingResourceWithIdentity struct{}
func (t *testingResourceWithIdentity) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) {
id := request.PriorState.GetAttr("id").AsString()
var exists bool
response.NewState, exists = store.Get(id)
if !exists {
response.NewState = cty.NullVal(TestingResourceSchema.Body.ImpliedType())
response.Identity = cty.UnknownVal(TestingResourceWithIdentitySchema.Identity.ImpliedType())
} else {
response.Identity = cty.StringVal("id:" + id)
}
return
}
func (t *testingResourceWithIdentity) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
if request.ProposedNewState.IsNull() {
response.PlannedState = request.ProposedNewState
return
}
response.PlannedState = planEnsureId(request.ProposedNewState)
response.PlannedIdentity = cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("id:" + response.PlannedState.GetAttr("id").AsString()),
})
replace, err := validateId(response.PlannedState, request.PriorState, store)
if err != nil {
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResourceWithIdentity error", err.Error()))
return
}
if replace {
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
}
return
}
func (t *testingResourceWithIdentity) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) {
if request.PlannedState.IsNull() {
store.Delete(request.PriorState.GetAttr("id").AsString())
response.NewState = request.PlannedState
return
}
value := applyEnsureId(request.PlannedState)
replace, err := validateId(value, request.PriorState, store)
if err != nil {
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResourceWithIdentity error", err.Error()))
return
}
response.NewState = value
response.NewIdentity = request.PlannedIdentity
if replace {
store.Delete(request.PriorState.GetAttr("id").AsString())
}
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
return
}
func validateId(target cty.Value, prior cty.Value, store *ResourceStore) (bool, error) {
if prior.IsNull() {
// Then we're creating a resource, we want to make sure we're not

Loading…
Cancel
Save