mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
* write-only attributes: internal providers should set write-only attributes to null * add changelog * fix copywrite headers
501 lines
17 KiB
Go
501 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package testing
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// resource is an interface that represents a resource that can be managed by
|
|
// the mock provider defined in this package.
|
|
type resource interface {
|
|
// Read reads the current state of the resource from the store.
|
|
Read(request providers.ReadResourceRequest, store *ResourceStore) providers.ReadResourceResponse
|
|
|
|
// Plan plans the resource for creation.
|
|
Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) providers.PlanResourceChangeResponse
|
|
|
|
// Apply applies the planned changes to the resource.
|
|
Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) providers.ApplyResourceChangeResponse
|
|
}
|
|
|
|
func getResource(name string) resource {
|
|
switch name {
|
|
case "testing_resource":
|
|
return &testingResource{}
|
|
case "testing_deferred_resource":
|
|
return &deferredResource{}
|
|
case "testing_failed_resource":
|
|
return &failedResource{}
|
|
case "testing_blocked_resource":
|
|
return &blockedResource{}
|
|
case "testing_write_only_resource":
|
|
return &writeOnlyResource{}
|
|
case "testing_resource_with_identity":
|
|
return &testingResourceWithIdentity{}
|
|
default:
|
|
panic("unknown resource: " + name)
|
|
}
|
|
}
|
|
|
|
var (
|
|
_ resource = (*testingResource)(nil)
|
|
_ resource = (*deferredResource)(nil)
|
|
_ resource = (*failedResource)(nil)
|
|
_ resource = (*blockedResource)(nil)
|
|
_ resource = (*writeOnlyResource)(nil)
|
|
_ resource = (*testingResourceWithIdentity)(nil)
|
|
)
|
|
|
|
// testingResource is a simple resource that can be managed by the mock provider
|
|
// defined in this package.
|
|
type testingResource struct{}
|
|
|
|
func (t *testingResource) 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())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *testingResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
|
|
if request.ProposedNewState.IsNull() {
|
|
response.PlannedState = request.ProposedNewState
|
|
return
|
|
}
|
|
|
|
response.PlannedState = planEnsureId(request.ProposedNewState)
|
|
replace, err := validateId(response.PlannedState, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
if replace {
|
|
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *testingResource) 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, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
response.NewState = value
|
|
|
|
if replace {
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
}
|
|
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
|
|
return
|
|
}
|
|
|
|
// deferredResource is a resource that can defer itself based on the provided
|
|
// configuration.
|
|
type deferredResource struct{}
|
|
|
|
func (d *deferredResource) 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(DeferredResourceSchema.Body.ImpliedType())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *deferredResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
|
|
if request.ProposedNewState.IsNull() {
|
|
if deferred := request.PriorState.GetAttr("deferred"); !deferred.IsNull() && deferred.IsKnown() && deferred.True() {
|
|
response.Deferred = &providers.Deferred{
|
|
Reason: providers.DeferredReasonResourceConfigUnknown,
|
|
}
|
|
}
|
|
response.PlannedState = request.ProposedNewState
|
|
return
|
|
}
|
|
|
|
response.PlannedState = planEnsureId(request.ProposedNewState)
|
|
replace, err := validateId(response.PlannedState, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "deferredResource error", err.Error()))
|
|
return
|
|
}
|
|
if deferred := response.PlannedState.GetAttr("deferred"); !deferred.IsNull() && deferred.IsKnown() && deferred.True() {
|
|
response.Deferred = &providers.Deferred{
|
|
Reason: providers.DeferredReasonResourceConfigUnknown,
|
|
}
|
|
}
|
|
if replace {
|
|
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *deferredResource) 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, "deferredResource error", err.Error()))
|
|
return
|
|
}
|
|
response.NewState = value
|
|
|
|
if replace {
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
}
|
|
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
|
|
return
|
|
}
|
|
|
|
// failedResource is a resource that can be set to fail during Plan or Apply.
|
|
type failedResource struct{}
|
|
|
|
func (f *failedResource) 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(FailedResourceSchema.Body.ImpliedType())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (f *failedResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
|
|
if request.ProposedNewState.IsNull() {
|
|
response.PlannedState = request.ProposedNewState
|
|
if attr := request.PriorState.GetAttr("fail_plan"); !attr.IsNull() && attr.IsKnown() && attr.True() {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during plan"))
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
response.PlannedState = planEnsureId(request.ProposedNewState)
|
|
replace, err := validateId(response.PlannedState, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", err.Error()))
|
|
return
|
|
}
|
|
|
|
setUnknown(response.PlannedState, "fail_apply")
|
|
setUnknown(response.PlannedState, "fail_plan")
|
|
|
|
if attr := response.PlannedState.GetAttr("fail_plan"); !attr.IsNull() && attr.IsKnown() && attr.True() {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during plan"))
|
|
}
|
|
if replace {
|
|
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (f *failedResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) {
|
|
if request.PlannedState.IsNull() {
|
|
if attr := request.PriorState.GetAttr("fail_apply"); !attr.IsNull() && attr.IsKnown() && attr.True() {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during apply"))
|
|
return
|
|
}
|
|
response.NewState = request.PlannedState
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
return
|
|
}
|
|
|
|
value := applyEnsureId(request.PlannedState)
|
|
replace, err := validateId(value, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
|
|
setKnown(value, "fail_apply", cty.False)
|
|
setKnown(value, "fail_plan", cty.False)
|
|
|
|
if attr := value.GetAttr("fail_apply"); !attr.IsNull() && attr.IsKnown() && attr.True() {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during apply"))
|
|
return
|
|
}
|
|
response.NewState = value
|
|
|
|
if replace {
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
}
|
|
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
|
|
return
|
|
}
|
|
|
|
// blockedResource is a resource that accepts a list of required resource ids
|
|
// and will fail to apply if those resources don't exist. They will also fail to
|
|
// destroy if the resources do not exist - this ensures they have to be created
|
|
// and destroyed in the correct order.
|
|
type blockedResource struct{}
|
|
|
|
func (b *blockedResource) 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(DeferredResourceSchema.Body.ImpliedType())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (b *blockedResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
|
|
if request.ProposedNewState.IsNull() {
|
|
response.PlannedState = request.ProposedNewState
|
|
return
|
|
}
|
|
|
|
response.PlannedState = planEnsureId(request.ProposedNewState)
|
|
replace, err := validateId(response.PlannedState, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
if replace {
|
|
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (b *blockedResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) {
|
|
if request.PlannedState.IsNull() {
|
|
if required := request.PriorState.GetAttr("required_resources"); !required.IsNull() && required.IsKnown() {
|
|
for _, id := range required.AsValueSlice() {
|
|
if _, exists := store.Get(id.AsString()); !exists {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "blockedResource error", fmt.Sprintf("required resource %q does not exists, so can't destroy self", id.AsString())))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
|
|
if required := value.GetAttr("required_resources"); !required.IsNull() && required.IsKnown() {
|
|
for _, id := range required.AsValueSlice() {
|
|
if _, exists := store.Get(id.AsString()); !exists {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "blockedResource error", fmt.Sprintf("required resource %q does not exist, so can't apply self", id.AsString())))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
response.NewState = value
|
|
|
|
if replace {
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
}
|
|
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
|
|
return
|
|
}
|
|
|
|
// writeOnlyResource is the same as testingResource but it includes an extra
|
|
// write-only attribute.
|
|
type writeOnlyResource struct{}
|
|
|
|
func (w *writeOnlyResource) 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(WriteOnlyResourceSchema.Body.ImpliedType())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (w *writeOnlyResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) {
|
|
if request.ProposedNewState.IsNull() {
|
|
response.PlannedState = request.ProposedNewState
|
|
return
|
|
}
|
|
|
|
response.PlannedState = setNull(planEnsureId(request.ProposedNewState), "write_only")
|
|
replace, err := validateId(response.PlannedState, request.PriorState, store)
|
|
if err != nil {
|
|
response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
if replace {
|
|
response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (w *writeOnlyResource) 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, "testingResource error", err.Error()))
|
|
return
|
|
}
|
|
response.NewState = value
|
|
|
|
if replace {
|
|
store.Delete(request.PriorState.GetAttr("id").AsString())
|
|
}
|
|
store.Set(response.NewState.GetAttr("id").AsString(), response.NewState)
|
|
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
|
|
// creating a resource with an existing ID.
|
|
if id := target.GetAttr("id"); id.IsKnown() {
|
|
if _, exists := store.Get(id.AsString()); exists {
|
|
return false, fmt.Errorf("resource with id %q already exists", id.AsString())
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
if attr := target.GetAttr("id"); !attr.IsKnown() {
|
|
// Then the attribute has been set to unknown, which means we're
|
|
// potentially changing the id.
|
|
return true, nil
|
|
}
|
|
|
|
// Now, we know that the ID is known in both the prior and target states.
|
|
if result := prior.GetAttr("id").Equals(target.GetAttr("id")); result.False() {
|
|
// Then the ID value is changing, so we need to delete the old ID
|
|
// and create the new one.
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func planEnsureId(value cty.Value) cty.Value {
|
|
return setUnknown(value, "id")
|
|
}
|
|
|
|
func applyEnsureId(value cty.Value) cty.Value {
|
|
return setKnown(value, "id", cty.StringVal(mustGenerateUUID()))
|
|
}
|
|
|
|
func setUnknown(value cty.Value, attr string) cty.Value {
|
|
if v := value.GetAttr(attr); v.IsNull() {
|
|
vals := value.AsValueMap()
|
|
vals[attr] = cty.UnknownVal(cty.String)
|
|
return cty.ObjectVal(vals)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func setKnown(value cty.Value, attr string, attrValue cty.Value) cty.Value {
|
|
if v := value.GetAttr(attr); !v.IsKnown() {
|
|
vals := value.AsValueMap()
|
|
vals[attr] = attrValue
|
|
return cty.ObjectVal(vals)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func setNull(value cty.Value, attr string) cty.Value {
|
|
if v := value.GetAttr(attr); !v.IsKnown() {
|
|
vals := value.AsValueMap()
|
|
vals[attr] = cty.NullVal(v.Type())
|
|
return cty.ObjectVal(vals)
|
|
}
|
|
return value
|
|
}
|