github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_abstract_test.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 tofu 7 8 import ( 9 "fmt" 10 "testing" 11 12 "github.com/opentofu/opentofu/internal/addrs" 13 "github.com/opentofu/opentofu/internal/configs" 14 "github.com/opentofu/opentofu/internal/configs/configschema" 15 "github.com/opentofu/opentofu/internal/providers" 16 "github.com/opentofu/opentofu/internal/states" 17 "github.com/zclconf/go-cty/cty" 18 ) 19 20 func TestNodeAbstractResourceProvider(t *testing.T) { 21 tests := []struct { 22 Addr addrs.ConfigResource 23 Config *configs.Resource 24 Want addrs.Provider 25 }{ 26 { 27 Addr: addrs.Resource{ 28 Mode: addrs.ManagedResourceMode, 29 Type: "null_resource", 30 Name: "baz", 31 }.InModule(addrs.RootModule), 32 Want: addrs.Provider{ 33 Hostname: addrs.DefaultProviderRegistryHost, 34 Namespace: "hashicorp", 35 Type: "null", 36 }, 37 }, 38 { 39 Addr: addrs.Resource{ 40 Mode: addrs.DataResourceMode, 41 Type: "terraform_remote_state", 42 Name: "baz", 43 }.InModule(addrs.RootModule), 44 Want: addrs.Provider{ 45 // As a special case, the type prefix "terraform_" maps to 46 // the builtin provider, not the default one. 47 Hostname: addrs.BuiltInProviderHost, 48 Namespace: addrs.BuiltInProviderNamespace, 49 Type: "terraform", 50 }, 51 }, 52 { 53 Addr: addrs.Resource{ 54 Mode: addrs.ManagedResourceMode, 55 Type: "null_resource", 56 Name: "baz", 57 }.InModule(addrs.RootModule), 58 Config: &configs.Resource{ 59 // Just enough configs.Resource for the Provider method. Not 60 // actually valid for general use. 61 Provider: addrs.Provider{ 62 Hostname: addrs.DefaultProviderRegistryHost, 63 Namespace: "awesomecorp", 64 Type: "happycloud", 65 }, 66 }, 67 // The config overrides the default behavior. 68 Want: addrs.Provider{ 69 Hostname: addrs.DefaultProviderRegistryHost, 70 Namespace: "awesomecorp", 71 Type: "happycloud", 72 }, 73 }, 74 { 75 Addr: addrs.Resource{ 76 Mode: addrs.DataResourceMode, 77 Type: "terraform_remote_state", 78 Name: "baz", 79 }.InModule(addrs.RootModule), 80 Config: &configs.Resource{ 81 // Just enough configs.Resource for the Provider method. Not 82 // actually valid for general use. 83 Provider: addrs.Provider{ 84 Hostname: addrs.DefaultProviderRegistryHost, 85 Namespace: "awesomecorp", 86 Type: "happycloud", 87 }, 88 }, 89 // The config overrides the default behavior. 90 Want: addrs.Provider{ 91 Hostname: addrs.DefaultProviderRegistryHost, 92 Namespace: "awesomecorp", 93 Type: "happycloud", 94 }, 95 }, 96 } 97 98 for _, test := range tests { 99 var name string 100 if test.Config != nil { 101 name = fmt.Sprintf("%s with configured %s", test.Addr, test.Config.Provider) 102 } else { 103 name = fmt.Sprintf("%s with no configuration", test.Addr) 104 } 105 t.Run(name, func(t *testing.T) { 106 node := &NodeAbstractResource{ 107 // Just enough NodeAbstractResource for the Provider function. 108 // (This would not be valid for some other functions.) 109 Addr: test.Addr, 110 Config: test.Config, 111 } 112 got := node.Provider() 113 if got != test.Want { 114 t.Errorf("wrong result\naddr: %s\nconfig: %#v\ngot: %s\nwant: %s", test.Addr, test.Config, got, test.Want) 115 } 116 }) 117 } 118 } 119 120 // Make sure ProvideBy returns the final resolved provider 121 func TestNodeAbstractResourceSetProvider(t *testing.T) { 122 node := &NodeAbstractResource{ 123 124 // Just enough NodeAbstractResource for the Provider function. 125 // (This would not be valid for some other functions.) 126 Addr: addrs.Resource{ 127 Mode: addrs.DataResourceMode, 128 Type: "terraform_remote_state", 129 Name: "baz", 130 }.InModule(addrs.RootModule), 131 Config: &configs.Resource{ 132 Mode: addrs.ManagedResourceMode, 133 Type: "terraform_remote_state", 134 Name: "baz", 135 // Just enough configs.Resource for the Provider method. Not 136 // actually valid for general use. 137 Provider: addrs.Provider{ 138 Hostname: addrs.DefaultProviderRegistryHost, 139 Namespace: "awesomecorp", 140 Type: "happycloud", 141 }, 142 }, 143 } 144 145 p, exact := node.ProvidedBy() 146 if exact { 147 t.Fatalf("no exact provider should be found from this confniguration, got %q\n", p) 148 } 149 150 // the implied non-exact provider should be "terraform" 151 lpc, ok := p.(addrs.LocalProviderConfig) 152 if !ok { 153 t.Fatalf("expected LocalProviderConfig, got %#v\n", p) 154 } 155 156 if lpc.LocalName != "terraform" { 157 t.Fatalf("expected non-exact provider of 'terraform', got %q", lpc.LocalName) 158 } 159 160 // now set a resolved provider for the resource 161 resolved := addrs.AbsProviderConfig{ 162 Provider: addrs.Provider{ 163 Hostname: addrs.DefaultProviderRegistryHost, 164 Namespace: "awesomecorp", 165 Type: "happycloud", 166 }, 167 Module: addrs.RootModule, 168 Alias: "test", 169 } 170 171 node.SetProvider(resolved) 172 p, exact = node.ProvidedBy() 173 if !exact { 174 t.Fatalf("exact provider should be found, got %q\n", p) 175 } 176 177 apc, ok := p.(addrs.AbsProviderConfig) 178 if !ok { 179 t.Fatalf("expected AbsProviderConfig, got %#v\n", p) 180 } 181 182 if apc.String() != resolved.String() { 183 t.Fatalf("incorrect resolved config: got %#v, wanted %#v\n", apc, resolved) 184 } 185 } 186 187 func TestNodeAbstractResource_ReadResourceInstanceState(t *testing.T) { 188 mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ 189 Attributes: map[string]*configschema.Attribute{ 190 "id": { 191 Type: cty.String, 192 Optional: true, 193 }, 194 }, 195 }) 196 // This test does not configure the provider, but the mock provider will 197 // check that this was called and report errors. 198 mockProvider.ConfigureProviderCalled = true 199 200 tests := map[string]struct { 201 State *states.State 202 Node *NodeAbstractResource 203 ExpectedInstanceId string 204 }{ 205 "ReadState gets primary instance state": { 206 State: states.BuildState(func(s *states.SyncState) { 207 providerAddr := addrs.AbsProviderConfig{ 208 Provider: addrs.NewDefaultProvider("aws"), 209 Module: addrs.RootModule, 210 } 211 oneAddr := addrs.Resource{ 212 Mode: addrs.ManagedResourceMode, 213 Type: "aws_instance", 214 Name: "bar", 215 }.Absolute(addrs.RootModuleInstance) 216 s.SetResourceProvider(oneAddr, providerAddr) 217 s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ 218 Status: states.ObjectReady, 219 AttrsJSON: []byte(`{"id":"i-abc123"}`), 220 }, providerAddr) 221 }), 222 Node: &NodeAbstractResource{ 223 Addr: mustConfigResourceAddr("aws_instance.bar"), 224 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 225 }, 226 ExpectedInstanceId: "i-abc123", 227 }, 228 } 229 230 for k, test := range tests { 231 t.Run(k, func(t *testing.T) { 232 ctx := new(MockEvalContext) 233 ctx.StateState = test.State.SyncWrapper() 234 ctx.PathPath = addrs.RootModuleInstance 235 ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() 236 237 ctx.ProviderProvider = providers.Interface(mockProvider) 238 239 got, readDiags := test.Node.readResourceInstanceState(ctx, test.Node.Addr.Resource.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) 240 if readDiags.HasErrors() { 241 t.Fatalf("[%s] Got err: %#v", k, readDiags.Err()) 242 } 243 244 expected := test.ExpectedInstanceId 245 246 if !(got != nil && got.Value.GetAttr("id") == cty.StringVal(expected)) { 247 t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, got) 248 } 249 }) 250 } 251 } 252 253 func TestNodeAbstractResource_ReadResourceInstanceStateDeposed(t *testing.T) { 254 mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ 255 Attributes: map[string]*configschema.Attribute{ 256 "id": { 257 Type: cty.String, 258 Optional: true, 259 }, 260 }, 261 }) 262 // This test does not configure the provider, but the mock provider will 263 // check that this was called and report errors. 264 mockProvider.ConfigureProviderCalled = true 265 266 tests := map[string]struct { 267 State *states.State 268 Node *NodeAbstractResource 269 ExpectedInstanceId string 270 }{ 271 "ReadStateDeposed gets deposed instance": { 272 State: states.BuildState(func(s *states.SyncState) { 273 providerAddr := addrs.AbsProviderConfig{ 274 Provider: addrs.NewDefaultProvider("aws"), 275 Module: addrs.RootModule, 276 } 277 oneAddr := addrs.Resource{ 278 Mode: addrs.ManagedResourceMode, 279 Type: "aws_instance", 280 Name: "bar", 281 }.Absolute(addrs.RootModuleInstance) 282 s.SetResourceProvider(oneAddr, providerAddr) 283 s.SetResourceInstanceDeposed(oneAddr.Instance(addrs.NoKey), states.DeposedKey("00000001"), &states.ResourceInstanceObjectSrc{ 284 Status: states.ObjectReady, 285 AttrsJSON: []byte(`{"id":"i-abc123"}`), 286 }, providerAddr) 287 }), 288 Node: &NodeAbstractResource{ 289 Addr: mustConfigResourceAddr("aws_instance.bar"), 290 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 291 }, 292 ExpectedInstanceId: "i-abc123", 293 }, 294 } 295 for k, test := range tests { 296 t.Run(k, func(t *testing.T) { 297 ctx := new(MockEvalContext) 298 ctx.StateState = test.State.SyncWrapper() 299 ctx.PathPath = addrs.RootModuleInstance 300 ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() 301 ctx.ProviderProvider = providers.Interface(mockProvider) 302 303 key := states.DeposedKey("00000001") // shim from legacy state assigns 0th deposed index this key 304 305 got, readDiags := test.Node.readResourceInstanceStateDeposed(ctx, test.Node.Addr.Resource.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), key) 306 if readDiags.HasErrors() { 307 t.Fatalf("[%s] Got err: %#v", k, readDiags.Err()) 308 } 309 310 expected := test.ExpectedInstanceId 311 312 if !(got != nil && got.Value.GetAttr("id") == cty.StringVal(expected)) { 313 t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, got) 314 } 315 }) 316 } 317 }