github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/backend_refresh_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package local 5 6 import ( 7 "context" 8 "fmt" 9 "strings" 10 "testing" 11 12 "github.com/terramate-io/tf/addrs" 13 "github.com/terramate-io/tf/backend" 14 "github.com/terramate-io/tf/command/arguments" 15 "github.com/terramate-io/tf/command/clistate" 16 "github.com/terramate-io/tf/command/views" 17 "github.com/terramate-io/tf/configs/configschema" 18 "github.com/terramate-io/tf/depsfile" 19 "github.com/terramate-io/tf/initwd" 20 "github.com/terramate-io/tf/providers" 21 "github.com/terramate-io/tf/states" 22 "github.com/terramate-io/tf/terminal" 23 "github.com/terramate-io/tf/terraform" 24 25 "github.com/zclconf/go-cty/cty" 26 ) 27 28 func TestLocal_refresh(t *testing.T) { 29 b := TestLocal(t) 30 31 p := TestLocalProvider(t, b, "test", refreshFixtureSchema()) 32 testStateFile(t, b.StatePath, testRefreshState()) 33 34 p.ReadResourceFn = nil 35 p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 36 "id": cty.StringVal("yes"), 37 })} 38 39 op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh") 40 defer configCleanup() 41 defer done(t) 42 43 run, err := b.Operation(context.Background(), op) 44 if err != nil { 45 t.Fatalf("bad: %s", err) 46 } 47 <-run.Done() 48 49 if !p.ReadResourceCalled { 50 t.Fatal("ReadResource should be called") 51 } 52 53 checkState(t, b.StateOutPath, ` 54 test_instance.foo: 55 ID = yes 56 provider = provider["registry.terraform.io/hashicorp/test"] 57 `) 58 59 // the backend should be unlocked after a run 60 assertBackendStateUnlocked(t, b) 61 } 62 63 func TestLocal_refreshInput(t *testing.T) { 64 b := TestLocal(t) 65 66 schema := providers.ProviderSchema{ 67 Provider: providers.Schema{ 68 Block: &configschema.Block{ 69 Attributes: map[string]*configschema.Attribute{ 70 "value": {Type: cty.String, Optional: true}, 71 }, 72 }, 73 }, 74 ResourceTypes: map[string]providers.Schema{ 75 "test_instance": { 76 Block: &configschema.Block{ 77 Attributes: map[string]*configschema.Attribute{ 78 "id": {Type: cty.String, Computed: true}, 79 "foo": {Type: cty.String, Optional: true}, 80 "ami": {Type: cty.String, Optional: true}, 81 }, 82 }, 83 }, 84 }, 85 } 86 87 p := TestLocalProvider(t, b, "test", schema) 88 testStateFile(t, b.StatePath, testRefreshState()) 89 90 p.ReadResourceFn = nil 91 p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 92 "id": cty.StringVal("yes"), 93 })} 94 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 95 val := req.Config.GetAttr("value") 96 if val.IsNull() || val.AsString() != "bar" { 97 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect value %#v", val)) 98 } 99 100 return 101 } 102 103 // Enable input asking since it is normally disabled by default 104 b.OpInput = true 105 b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"} 106 107 op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-var-unset") 108 defer configCleanup() 109 defer done(t) 110 op.UIIn = b.ContextOpts.UIInput 111 112 run, err := b.Operation(context.Background(), op) 113 if err != nil { 114 t.Fatalf("bad: %s", err) 115 } 116 <-run.Done() 117 118 if !p.ReadResourceCalled { 119 t.Fatal("ReadResource should be called") 120 } 121 122 checkState(t, b.StateOutPath, ` 123 test_instance.foo: 124 ID = yes 125 provider = provider["registry.terraform.io/hashicorp/test"] 126 `) 127 } 128 129 func TestLocal_refreshValidate(t *testing.T) { 130 b := TestLocal(t) 131 p := TestLocalProvider(t, b, "test", refreshFixtureSchema()) 132 testStateFile(t, b.StatePath, testRefreshState()) 133 p.ReadResourceFn = nil 134 p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 135 "id": cty.StringVal("yes"), 136 })} 137 138 // Enable validation 139 b.OpValidation = true 140 141 op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh") 142 defer configCleanup() 143 defer done(t) 144 145 run, err := b.Operation(context.Background(), op) 146 if err != nil { 147 t.Fatalf("bad: %s", err) 148 } 149 <-run.Done() 150 151 checkState(t, b.StateOutPath, ` 152 test_instance.foo: 153 ID = yes 154 provider = provider["registry.terraform.io/hashicorp/test"] 155 `) 156 } 157 158 func TestLocal_refreshValidateProviderConfigured(t *testing.T) { 159 b := TestLocal(t) 160 161 schema := providers.ProviderSchema{ 162 Provider: providers.Schema{ 163 Block: &configschema.Block{ 164 Attributes: map[string]*configschema.Attribute{ 165 "value": {Type: cty.String, Optional: true}, 166 }, 167 }, 168 }, 169 ResourceTypes: map[string]providers.Schema{ 170 "test_instance": { 171 Block: &configschema.Block{ 172 Attributes: map[string]*configschema.Attribute{ 173 "id": {Type: cty.String, Computed: true}, 174 "ami": {Type: cty.String, Optional: true}, 175 }, 176 }, 177 }, 178 }, 179 } 180 181 p := TestLocalProvider(t, b, "test", schema) 182 testStateFile(t, b.StatePath, testRefreshState()) 183 p.ReadResourceFn = nil 184 p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 185 "id": cty.StringVal("yes"), 186 })} 187 188 // Enable validation 189 b.OpValidation = true 190 191 op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-provider-config") 192 defer configCleanup() 193 defer done(t) 194 195 run, err := b.Operation(context.Background(), op) 196 if err != nil { 197 t.Fatalf("bad: %s", err) 198 } 199 <-run.Done() 200 201 if !p.ValidateProviderConfigCalled { 202 t.Fatal("Validate provider config should be called") 203 } 204 205 checkState(t, b.StateOutPath, ` 206 test_instance.foo: 207 ID = yes 208 provider = provider["registry.terraform.io/hashicorp/test"] 209 `) 210 } 211 212 // This test validates the state lacking behavior when the inner call to 213 // Context() fails 214 func TestLocal_refresh_context_error(t *testing.T) { 215 b := TestLocal(t) 216 testStateFile(t, b.StatePath, testRefreshState()) 217 op, configCleanup, done := testOperationRefresh(t, "./testdata/apply") 218 defer configCleanup() 219 defer done(t) 220 221 // we coerce a failure in Context() by omitting the provider schema 222 223 run, err := b.Operation(context.Background(), op) 224 if err != nil { 225 t.Fatalf("bad: %s", err) 226 } 227 <-run.Done() 228 if run.Result == backend.OperationSuccess { 229 t.Fatal("operation succeeded; want failure") 230 } 231 assertBackendStateUnlocked(t, b) 232 } 233 234 func TestLocal_refreshEmptyState(t *testing.T) { 235 b := TestLocal(t) 236 237 p := TestLocalProvider(t, b, "test", refreshFixtureSchema()) 238 testStateFile(t, b.StatePath, states.NewState()) 239 240 p.ReadResourceFn = nil 241 p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 242 "id": cty.StringVal("yes"), 243 })} 244 245 op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh") 246 defer configCleanup() 247 248 run, err := b.Operation(context.Background(), op) 249 if err != nil { 250 t.Fatalf("bad: %s", err) 251 } 252 <-run.Done() 253 254 output := done(t) 255 256 if stderr := output.Stderr(); stderr != "" { 257 t.Fatalf("expected only warning diags, got errors: %s", stderr) 258 } 259 if got, want := output.Stdout(), "Warning: Empty or non-existent state"; !strings.Contains(got, want) { 260 t.Errorf("wrong diags\n got: %s\nwant: %s", got, want) 261 } 262 263 // the backend should be unlocked after a run 264 assertBackendStateUnlocked(t, b) 265 } 266 267 func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 268 t.Helper() 269 270 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") 271 272 streams, done := terminal.StreamsForTesting(t) 273 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 274 275 // Many of our tests use an overridden "test" provider that's just in-memory 276 // inside the test process, not a separate plugin on disk. 277 depLocks := depsfile.NewLocks() 278 depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) 279 280 return &backend.Operation{ 281 Type: backend.OperationTypeRefresh, 282 ConfigDir: configDir, 283 ConfigLoader: configLoader, 284 StateLocker: clistate.NewNoopLocker(), 285 View: view, 286 DependencyLocks: depLocks, 287 }, configCleanup, done 288 } 289 290 // testRefreshState is just a common state that we use for testing refresh. 291 func testRefreshState() *states.State { 292 state := states.NewState() 293 root := state.EnsureModule(addrs.RootModuleInstance) 294 root.SetResourceInstanceCurrent( 295 mustResourceInstanceAddr("test_instance.foo").Resource, 296 &states.ResourceInstanceObjectSrc{ 297 Status: states.ObjectReady, 298 AttrsJSON: []byte(`{"id":"bar"}`), 299 }, 300 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 301 ) 302 return state 303 } 304 305 // refreshFixtureSchema returns a schema suitable for processing the 306 // configuration in testdata/refresh . This schema should be 307 // assigned to a mock provider named "test". 308 func refreshFixtureSchema() providers.ProviderSchema { 309 return providers.ProviderSchema{ 310 ResourceTypes: map[string]providers.Schema{ 311 "test_instance": { 312 Block: &configschema.Block{ 313 Attributes: map[string]*configschema.Attribute{ 314 "ami": {Type: cty.String, Optional: true}, 315 "id": {Type: cty.String, Computed: true}, 316 }, 317 }, 318 }, 319 }, 320 } 321 }