github.com/opentofu/opentofu@v1.7.1/internal/builtin/providers/tf/resource_data.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tf
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/hashicorp/go-uuid"
    12  	"github.com/opentofu/opentofu/internal/configs/configschema"
    13  	"github.com/opentofu/opentofu/internal/providers"
    14  	"github.com/opentofu/opentofu/internal/tfdiags"
    15  	"github.com/zclconf/go-cty/cty"
    16  	ctyjson "github.com/zclconf/go-cty/cty/json"
    17  )
    18  
    19  func dataStoreResourceSchema() providers.Schema {
    20  	return providers.Schema{
    21  		Block: &configschema.Block{
    22  			Attributes: map[string]*configschema.Attribute{
    23  				"input":            {Type: cty.DynamicPseudoType, Optional: true},
    24  				"output":           {Type: cty.DynamicPseudoType, Computed: true},
    25  				"triggers_replace": {Type: cty.DynamicPseudoType, Optional: true},
    26  				"id":               {Type: cty.String, Computed: true},
    27  			},
    28  		},
    29  	}
    30  }
    31  
    32  func validateDataStoreResourceConfig(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) {
    33  	if req.Config.IsNull() {
    34  		return resp
    35  	}
    36  
    37  	// Core does not currently validate computed values are not set in the
    38  	// configuration.
    39  	for _, attr := range []string{"id", "output"} {
    40  		if !req.Config.GetAttr(attr).IsNull() {
    41  			resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf(`%q attribute is read-only`, attr))
    42  		}
    43  	}
    44  	return resp
    45  }
    46  
    47  func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
    48  	ty := dataStoreResourceSchema().Block.ImpliedType()
    49  	val, err := ctyjson.Unmarshal(req.RawStateJSON, ty)
    50  	if err != nil {
    51  		resp.Diagnostics = resp.Diagnostics.Append(err)
    52  		return resp
    53  	}
    54  
    55  	resp.UpgradedState = val
    56  	return resp
    57  }
    58  
    59  func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
    60  	resp.NewState = req.PriorState
    61  	return resp
    62  }
    63  
    64  func planDataStoreResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
    65  	if req.ProposedNewState.IsNull() {
    66  		// destroy op
    67  		resp.PlannedState = req.ProposedNewState
    68  		return resp
    69  	}
    70  
    71  	planned := req.ProposedNewState.AsValueMap()
    72  
    73  	input := req.ProposedNewState.GetAttr("input")
    74  	trigger := req.ProposedNewState.GetAttr("triggers_replace")
    75  
    76  	switch {
    77  	case req.PriorState.IsNull():
    78  		// Create
    79  		// Set the id value to unknown.
    80  		planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
    81  
    82  		// Output type must always match the input, even when it's null.
    83  		if input.IsNull() {
    84  			planned["output"] = input
    85  		} else {
    86  			planned["output"] = cty.UnknownVal(input.Type())
    87  		}
    88  
    89  		resp.PlannedState = cty.ObjectVal(planned)
    90  		return resp
    91  
    92  	case !req.PriorState.GetAttr("triggers_replace").RawEquals(trigger):
    93  		// trigger changed, so we need to replace the entire instance
    94  		resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("triggers_replace"))
    95  		planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
    96  
    97  		// We need to check the input for the replacement instance to compute a
    98  		// new output.
    99  		if input.IsNull() {
   100  			planned["output"] = input
   101  		} else {
   102  			planned["output"] = cty.UnknownVal(input.Type())
   103  		}
   104  
   105  	case !req.PriorState.GetAttr("input").RawEquals(input):
   106  		// only input changed, so we only need to re-compute output
   107  		planned["output"] = cty.UnknownVal(input.Type())
   108  	}
   109  
   110  	resp.PlannedState = cty.ObjectVal(planned)
   111  	return resp
   112  }
   113  
   114  var testUUIDHook func() string
   115  
   116  func applyDataStoreResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
   117  	if req.PlannedState.IsNull() {
   118  		resp.NewState = req.PlannedState
   119  		return resp
   120  	}
   121  
   122  	newState := req.PlannedState.AsValueMap()
   123  
   124  	if !req.PlannedState.GetAttr("output").IsKnown() {
   125  		newState["output"] = req.PlannedState.GetAttr("input")
   126  	}
   127  
   128  	if !req.PlannedState.GetAttr("id").IsKnown() {
   129  		idString, err := uuid.GenerateUUID()
   130  		// OpenTofu would probably never get this far without a good random
   131  		// source, but catch the error anyway.
   132  		if err != nil {
   133  			diag := tfdiags.AttributeValue(
   134  				tfdiags.Error,
   135  				"Error generating id",
   136  				err.Error(),
   137  				cty.GetAttrPath("id"),
   138  			)
   139  
   140  			resp.Diagnostics = resp.Diagnostics.Append(diag)
   141  		}
   142  
   143  		if testUUIDHook != nil {
   144  			idString = testUUIDHook()
   145  		}
   146  
   147  		newState["id"] = cty.StringVal(idString)
   148  	}
   149  
   150  	resp.NewState = cty.ObjectVal(newState)
   151  
   152  	return resp
   153  }
   154  
   155  // TODO: This isn't very useful even for examples, because terraform_data has
   156  // no way to refresh the full resource value from only the import ID. This
   157  // minimal implementation allows the import to succeed, and can be extended
   158  // once the configuration is available during import.
   159  func importDataStore(req providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) {
   160  	schema := dataStoreResourceSchema()
   161  	v := cty.ObjectVal(map[string]cty.Value{
   162  		"id": cty.StringVal(req.ID),
   163  	})
   164  	state, err := schema.Block.CoerceValue(v)
   165  	resp.Diagnostics = resp.Diagnostics.Append(err)
   166  
   167  	resp.ImportedResources = []providers.ImportedResource{
   168  		{
   169  			TypeName: req.TypeName,
   170  			State:    state,
   171  		},
   172  	}
   173  	return resp
   174  }