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 }