|
4 | 4 | package resource |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "context" |
| 8 | + "fmt" |
| 9 | + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" |
| 10 | + "github.com/hashicorp/terraform-plugin-testing/terraform" |
7 | 11 | "testing" |
8 | 12 |
|
9 | 13 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" |
@@ -102,3 +106,354 @@ func TestTest_TestStep_ImportBlockId(t *testing.T) { |
102 | 106 | }, |
103 | 107 | }) |
104 | 108 | } |
| 109 | + |
| 110 | +func TestTest_TestStep_ImportBlockId_SkipDataSourceState(t *testing.T) { |
| 111 | + t.Parallel() |
| 112 | + |
| 113 | + UnitTest(t, TestCase{ |
| 114 | + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ |
| 115 | + tfversion.SkipBelow(tfversion.Version1_5_0), // ProtoV6ProviderFactories |
| 116 | + }, |
| 117 | + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ |
| 118 | + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ |
| 119 | + DataSources: map[string]testprovider.DataSource{ |
| 120 | + "examplecloud_thing": { |
| 121 | + ReadResponse: &datasource.ReadResponse{ |
| 122 | + State: tftypes.NewValue( |
| 123 | + tftypes.Object{ |
| 124 | + AttributeTypes: map[string]tftypes.Type{ |
| 125 | + "id": tftypes.String, |
| 126 | + }, |
| 127 | + }, |
| 128 | + map[string]tftypes.Value{ |
| 129 | + "id": tftypes.NewValue(tftypes.String, "datasource-test"), |
| 130 | + }, |
| 131 | + ), |
| 132 | + }, |
| 133 | + SchemaResponse: &datasource.SchemaResponse{ |
| 134 | + Schema: &tfprotov6.Schema{ |
| 135 | + Block: &tfprotov6.SchemaBlock{ |
| 136 | + Attributes: []*tfprotov6.SchemaAttribute{ |
| 137 | + { |
| 138 | + Name: "id", |
| 139 | + Type: tftypes.String, |
| 140 | + Computed: true, |
| 141 | + }, |
| 142 | + }, |
| 143 | + }, |
| 144 | + }, |
| 145 | + }, |
| 146 | + }, |
| 147 | + }, |
| 148 | + Resources: map[string]testprovider.Resource{ |
| 149 | + "examplecloud_thing": { |
| 150 | + CreateResponse: &resource.CreateResponse{ |
| 151 | + NewState: tftypes.NewValue( |
| 152 | + tftypes.Object{ |
| 153 | + AttributeTypes: map[string]tftypes.Type{ |
| 154 | + "id": tftypes.String, |
| 155 | + }, |
| 156 | + }, |
| 157 | + map[string]tftypes.Value{ |
| 158 | + "id": tftypes.NewValue(tftypes.String, "resource-test"), |
| 159 | + }, |
| 160 | + ), |
| 161 | + }, |
| 162 | + ImportStateResponse: &resource.ImportStateResponse{ |
| 163 | + State: tftypes.NewValue( |
| 164 | + tftypes.Object{ |
| 165 | + AttributeTypes: map[string]tftypes.Type{ |
| 166 | + "id": tftypes.String, |
| 167 | + }, |
| 168 | + }, |
| 169 | + map[string]tftypes.Value{ |
| 170 | + "id": tftypes.NewValue(tftypes.String, "resource-test"), |
| 171 | + }, |
| 172 | + ), |
| 173 | + }, |
| 174 | + SchemaResponse: &resource.SchemaResponse{ |
| 175 | + Schema: &tfprotov6.Schema{ |
| 176 | + Block: &tfprotov6.SchemaBlock{ |
| 177 | + Attributes: []*tfprotov6.SchemaAttribute{ |
| 178 | + { |
| 179 | + Name: "id", |
| 180 | + Type: tftypes.String, |
| 181 | + Computed: true, |
| 182 | + }, |
| 183 | + }, |
| 184 | + }, |
| 185 | + }, |
| 186 | + }, |
| 187 | + }, |
| 188 | + }, |
| 189 | + }), |
| 190 | + }, |
| 191 | + Steps: []TestStep{ |
| 192 | + { |
| 193 | + Config: ` |
| 194 | + data "examplecloud_thing" "test" {} |
| 195 | + resource "examplecloud_thing" "test" {} |
| 196 | + `, |
| 197 | + }, |
| 198 | + { |
| 199 | + ResourceName: "examplecloud_thing.test", |
| 200 | + ImportState: true, |
| 201 | + ImportStateKind: ImportBlockWithId, |
| 202 | + ImportStateCheck: func(is []*terraform.InstanceState) error { |
| 203 | + if len(is) > 1 { |
| 204 | + return fmt.Errorf("expected 1 state, got: %d", len(is)) |
| 205 | + } |
| 206 | + |
| 207 | + return nil |
| 208 | + }, |
| 209 | + }, |
| 210 | + }, |
| 211 | + }) |
| 212 | +} |
| 213 | + |
| 214 | +func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore_Real_Example(t *testing.T) { |
| 215 | + /* |
| 216 | + This test tries to imitate a real world example of behaviour we often see in the AzureRM provider which requires |
| 217 | + the use of `ImportStateVerifyIgnore` when testing the import of a resource. |
| 218 | +
|
| 219 | + A sensitive field e.g. a password can be supplied on create but isn't returned in the API response on a subsequent |
| 220 | + read, resulting in a different value for password in the two states. |
| 221 | +
|
| 222 | + In the AzureRM provider this is usually handled one of two ways, both requiring `ImportStateVerifyIgnore` to make |
| 223 | + the test pass: |
| 224 | +
|
| 225 | + 1. Property doesn't get set in the read |
| 226 | + * in pluginSDK at create the config gets written to state because that's what we're expecting |
| 227 | + * the subsequent read updates the values to create a post-apply diff and update computed values |
| 228 | + * since we don't do anything to the property in the read the imported resource's state has the password missing |
| 229 | + compared to the created resource's state |
| 230 | +
|
| 231 | + 2. We retrieve the value from config and set that into state |
| 232 | + * the config isn't available at import time using only the import command (I think?) so there is nothing to |
| 233 | + retrieve and set into state when importing |
| 234 | +
|
| 235 | + For this test to pass I needed to add a `PlanChangeFunc` to the resource to set the id to a known value in the plan - see comment in the `PlanChangeFunc` |
| 236 | +
|
| 237 | + I also need to omit the `password` in the import config, otherwise the value in the config is used when importing the resource and the test |
| 238 | + ends up passing regardless of whether `ImportStateVerifyIgnore` has been specified or not |
| 239 | +
|
| 240 | + Ultimately it looks like: |
| 241 | + * Terraform is saying there's a bug in the provider? (see comment in `PlanChangeFunc`) |
| 242 | + * The import behaviour using a block vs. the command appears to differ |
| 243 | + */ |
| 244 | + t.Parallel() |
| 245 | + |
| 246 | + UnitTest(t, TestCase{ |
| 247 | + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ |
| 248 | + tfversion.SkipBelow(tfversion.Version1_5_0), // ProtoV6ProviderFactories |
| 249 | + }, |
| 250 | + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ |
| 251 | + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ |
| 252 | + Resources: map[string]testprovider.Resource{ |
| 253 | + "examplecloud_container": { |
| 254 | + CreateResponse: &resource.CreateResponse{ |
| 255 | + NewState: tftypes.NewValue( |
| 256 | + tftypes.Object{ |
| 257 | + AttributeTypes: map[string]tftypes.Type{ |
| 258 | + "id": tftypes.String, |
| 259 | + "name": tftypes.String, |
| 260 | + "password": tftypes.String, |
| 261 | + }, |
| 262 | + }, |
| 263 | + map[string]tftypes.Value{ |
| 264 | + "id": tftypes.NewValue(tftypes.String, "sometestid"), |
| 265 | + "name": tftypes.NewValue(tftypes.String, "somename"), |
| 266 | + "password": tftypes.NewValue(tftypes.String, "somevalue"), |
| 267 | + }, |
| 268 | + ), |
| 269 | + }, |
| 270 | + ImportStateResponse: &resource.ImportStateResponse{ |
| 271 | + State: tftypes.NewValue( |
| 272 | + tftypes.Object{ |
| 273 | + AttributeTypes: map[string]tftypes.Type{ |
| 274 | + "id": tftypes.String, |
| 275 | + "name": tftypes.String, |
| 276 | + "password": tftypes.String, |
| 277 | + }, |
| 278 | + }, |
| 279 | + map[string]tftypes.Value{ |
| 280 | + "id": tftypes.NewValue(tftypes.String, "sometestid"), |
| 281 | + "name": tftypes.NewValue(tftypes.String, "somename"), |
| 282 | + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing |
| 283 | + }, |
| 284 | + ), |
| 285 | + }, |
| 286 | + PlanChangeFunc: func(ctx context.Context, request resource.PlanChangeRequest, response *resource.PlanChangeResponse) { |
| 287 | + /* |
| 288 | + Returning a nil for another attribute to simulate a situation where `ImportStateVerifyIgnore` |
| 289 | + should be used results in the error below from Terraform |
| 290 | +
|
| 291 | + Error: Provider returned invalid result object after apply |
| 292 | +
|
| 293 | + After the apply operation, the provider still indicated an unknown value for |
| 294 | + examplecloud_container.test.id. All values must be known after apply, so this |
| 295 | + is always a bug in the provider and should be reported in the provider's own |
| 296 | + repository. Terraform will still save the other known object values in the |
| 297 | + state. |
| 298 | +
|
| 299 | + Modifying the plan to set the id to a known value appears to be the only way to |
| 300 | + circumvent this behaviour, the cause of which I don't fully understand |
| 301 | +
|
| 302 | + This doesn't seem great, because this gets applied to all Plans that happen in this |
| 303 | + test - so we're modifying plans in steps that we might not want to. |
| 304 | + */ |
| 305 | + |
| 306 | + objVal := map[string]tftypes.Value{} |
| 307 | + |
| 308 | + if !response.PlannedState.IsNull() { |
| 309 | + _ = response.PlannedState.As(&objVal) |
| 310 | + objVal["id"] = tftypes.NewValue(tftypes.String, "sometestid") |
| 311 | + } |
| 312 | + }, |
| 313 | + SchemaResponse: &resource.SchemaResponse{ |
| 314 | + Schema: &tfprotov6.Schema{ |
| 315 | + Block: &tfprotov6.SchemaBlock{ |
| 316 | + Attributes: []*tfprotov6.SchemaAttribute{ |
| 317 | + { |
| 318 | + Name: "id", |
| 319 | + Type: tftypes.String, |
| 320 | + Computed: true, |
| 321 | + }, |
| 322 | + { |
| 323 | + Name: "name", |
| 324 | + Type: tftypes.String, |
| 325 | + Required: true, |
| 326 | + }, |
| 327 | + { |
| 328 | + Name: "password", |
| 329 | + Type: tftypes.String, |
| 330 | + Optional: true, |
| 331 | + }, |
| 332 | + }, |
| 333 | + }, |
| 334 | + }, |
| 335 | + }, |
| 336 | + }, |
| 337 | + }, |
| 338 | + }), |
| 339 | + }, |
| 340 | + Steps: []TestStep{ |
| 341 | + { |
| 342 | + Config: ` |
| 343 | + resource "examplecloud_container" "test" { |
| 344 | + name = "somename" |
| 345 | + password = "somevalue" |
| 346 | + }`, |
| 347 | + }, |
| 348 | + { |
| 349 | + Config: ` |
| 350 | + terraform { |
| 351 | + required_providers { |
| 352 | + examplecloud = { |
| 353 | + source = "registry.terraform.io/hashicorp/examplecloud" |
| 354 | + } |
| 355 | + } |
| 356 | + } |
| 357 | +
|
| 358 | + resource "examplecloud_container" "test" { |
| 359 | + name = "somename" |
| 360 | + } |
| 361 | +
|
| 362 | + import { |
| 363 | + to = examplecloud_container.test |
| 364 | + id = "sometestid" |
| 365 | + }`, |
| 366 | + ResourceName: "examplecloud_container.test", |
| 367 | + ImportState: true, |
| 368 | + ImportStateKind: ImportBlockWithId, |
| 369 | + ImportStateVerify: true, |
| 370 | + ImportStateVerifyIgnore: []string{"password"}, |
| 371 | + }, |
| 372 | + }, |
| 373 | + }) |
| 374 | +} |
| 375 | + |
| 376 | +func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore(t *testing.T) { |
| 377 | + t.Parallel() |
| 378 | + |
| 379 | + UnitTest(t, TestCase{ |
| 380 | + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ |
| 381 | + tfversion.SkipBelow(tfversion.Version1_5_0), // ProtoV6ProviderFactories |
| 382 | + }, |
| 383 | + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ |
| 384 | + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ |
| 385 | + Resources: map[string]testprovider.Resource{ |
| 386 | + "examplecloud_container": { |
| 387 | + CreateResponse: &resource.CreateResponse{ |
| 388 | + NewState: tftypes.NewValue( |
| 389 | + tftypes.Object{ |
| 390 | + AttributeTypes: map[string]tftypes.Type{ |
| 391 | + "id": tftypes.String, |
| 392 | + "name": tftypes.String, |
| 393 | + "password": tftypes.String, |
| 394 | + }, |
| 395 | + }, |
| 396 | + map[string]tftypes.Value{ |
| 397 | + "id": tftypes.NewValue(tftypes.String, "sometestid"), |
| 398 | + "name": tftypes.NewValue(tftypes.String, "somename"), |
| 399 | + "password": tftypes.NewValue(tftypes.String, "somevalue"), |
| 400 | + }, |
| 401 | + ), |
| 402 | + }, |
| 403 | + ImportStateResponse: &resource.ImportStateResponse{ |
| 404 | + State: tftypes.NewValue( |
| 405 | + tftypes.Object{ |
| 406 | + AttributeTypes: map[string]tftypes.Type{ |
| 407 | + "id": tftypes.String, |
| 408 | + "name": tftypes.String, |
| 409 | + "password": tftypes.String, |
| 410 | + }, |
| 411 | + }, |
| 412 | + map[string]tftypes.Value{ |
| 413 | + "id": tftypes.NewValue(tftypes.String, "sometestid"), |
| 414 | + "name": tftypes.NewValue(tftypes.String, "somename"), |
| 415 | + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing |
| 416 | + }, |
| 417 | + ), |
| 418 | + }, |
| 419 | + SchemaResponse: &resource.SchemaResponse{ |
| 420 | + Schema: &tfprotov6.Schema{ |
| 421 | + Block: &tfprotov6.SchemaBlock{ |
| 422 | + Attributes: []*tfprotov6.SchemaAttribute{ |
| 423 | + { |
| 424 | + Name: "id", |
| 425 | + Type: tftypes.String, |
| 426 | + Computed: true, |
| 427 | + }, |
| 428 | + { |
| 429 | + Name: "name", |
| 430 | + Type: tftypes.String, |
| 431 | + Computed: true, |
| 432 | + }, |
| 433 | + { |
| 434 | + Name: "password", |
| 435 | + Type: tftypes.String, |
| 436 | + Computed: true, |
| 437 | + }, |
| 438 | + }, |
| 439 | + }, |
| 440 | + }, |
| 441 | + }, |
| 442 | + }, |
| 443 | + }, |
| 444 | + }), |
| 445 | + }, |
| 446 | + Steps: []TestStep{ |
| 447 | + { |
| 448 | + Config: `resource "examplecloud_container" "test" {}`, |
| 449 | + }, |
| 450 | + { |
| 451 | + ResourceName: "examplecloud_container.test", |
| 452 | + ImportState: true, |
| 453 | + ImportStateKind: ImportBlockWithId, |
| 454 | + ImportStateVerify: true, |
| 455 | + ImportStateVerifyIgnore: []string{"password"}, |
| 456 | + }, |
| 457 | + }, |
| 458 | + }) |
| 459 | +} |
0 commit comments