Skip to content

Commit 955994d

Browse files
committed
ImportBlockWithResourceIdentity
1 parent aafb9fe commit 955994d

File tree

3 files changed

+212
-42
lines changed

3 files changed

+212
-42
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package importstate_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
12+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
13+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
14+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
15+
16+
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
17+
)
18+
19+
func Test_ResourceIdentity(t *testing.T) {
20+
t.Parallel()
21+
22+
r.UnitTest(t, r.TestCase{
23+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
24+
tfversion.SkipBelow(tfversion.Version1_12_0), // TODO: ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later
25+
},
26+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
27+
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
28+
Resources: map[string]testprovider.Resource{
29+
"examplecloud_container": examplecloudResource(),
30+
},
31+
ResourceIdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{
32+
"examplecloud_container": {
33+
Version: 1,
34+
IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{
35+
{
36+
Name: "id",
37+
Type: tftypes.String,
38+
RequiredForImport: true,
39+
},
40+
},
41+
},
42+
},
43+
}),
44+
},
45+
Steps: []r.TestStep{
46+
{
47+
Config: `
48+
resource "examplecloud_container" "test" {
49+
location = "westeurope"
50+
name = "somevalue"
51+
}`,
52+
},
53+
{
54+
ResourceName: "examplecloud_container.test",
55+
ImportState: true,
56+
ImportStateKind: r.ImportBlockWithResourceIdentity,
57+
},
58+
},
59+
})
60+
}

helper/resource/testing.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/mitchellh/go-testing-interface"
1919

20+
"github.com/hashicorp/go-version"
2021
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
2122
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
2223

@@ -457,14 +458,48 @@ type ExternalProvider struct {
457458
type ImportStateKind byte
458459

459460
const (
460-
// ImportCommandWithId imports the state using the import command
461-
ImportCommandWithId ImportStateKind = iota
462-
// ImportBlockWithId imports the state using an import block with an ID
463-
ImportBlockWithId
461+
// ImportCommandWithID imports the state using the import command
462+
ImportCommandWithID ImportStateKind = iota
463+
464+
// ImportBlockWithID imports the state using an import block with an ID
465+
ImportBlockWithID
466+
464467
// ImportBlockWithResourceIdentity imports the state using an import block with a resource identity
465468
ImportBlockWithResourceIdentity
466469
)
467470

471+
// plannable returns true if this ImportStateKind uses the plannable import feature
472+
func (kind ImportStateKind) plannable() bool {
473+
return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity
474+
}
475+
476+
// resourceIdentity returns true if this ImportStateKind uses resource identity
477+
func (kind ImportStateKind) resourceIdentity() bool {
478+
return kind == ImportBlockWithResourceIdentity
479+
}
480+
481+
// terraformVersion returns the minimum Terraform version that supports
482+
// the features required for this ImportStateKind.
483+
func (kind ImportStateKind) terraformVersion() *version.Version {
484+
switch kind {
485+
case ImportBlockWithID:
486+
return tfversion.Version1_5_0
487+
case ImportBlockWithResourceIdentity:
488+
return tfversion.Version1_12_0
489+
default:
490+
return tfversion.Version0_12_26 // Default to the earlist version supported by the testing framework
491+
}
492+
493+
}
494+
495+
func (kind ImportStateKind) String() string {
496+
return map[ImportStateKind]string{
497+
ImportCommandWithID: "ImportCommandWithID",
498+
ImportBlockWithID: "ImportBlockWithID",
499+
ImportBlockWithResourceIdentity: "ImportBlockWithResourceIdentity",
500+
}[kind]
501+
}
502+
468503
// TestStep is a single apply sequence of a test, done within the
469504
// context of a state.
470505
//

helper/resource/testing_new_import_state.go

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,16 @@ import (
2424
"github.com/hashicorp/terraform-plugin-testing/tfversion"
2525
)
2626

27-
func requirePlannableImport(t testing.T, versionUnderTest version.Version) error {
28-
t.Helper()
29-
30-
if versionUnderTest.LessThan(tfversion.Version1_5_0) {
31-
return fmt.Errorf(
32-
`ImportState steps using plannable import blocks require Terraform 1.5.0 or later. Either ` +
33-
`upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` +
34-
`the test case to skip this test.` + "\n\n" +
35-
`https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`)
36-
}
37-
38-
return nil
39-
}
40-
4127
func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfgRaw string, providers *providerFactories, stepNumber int) error {
4228
t.Helper()
4329

44-
// Currently import modes `ImportBlockWithId` and `ImportBlockWithResourceIdentity` cannot support config file or directory
45-
// since these modes append the import block to the configuration automatically
46-
if step.ImportStateKind != ImportCommandWithId {
47-
if step.ConfigFile != nil || step.ConfigDirectory != nil {
48-
t.Fatalf("ImportStateKind %q is not supported for config file or directory", step.ImportStateKind)
49-
}
50-
}
51-
52-
if step.ImportStateKind != ImportCommandWithId {
53-
if err := requirePlannableImport(t, *helper.TerraformVersion()); err != nil {
54-
return err
55-
}
30+
// step.ImportStateKind implicitly defaults to the zero-value (ImportCommandWithID) for backward compatibility
31+
kind := step.ImportStateKind
32+
// Instead of calling [t.Fatal], return an error. This package's unit tests can use [TestStep.ExpectError] to match on the error message.
33+
// An alternative, [plugintest.TestExpectTFatal], does not have access to logged error messages, so it is open to false positives on this
34+
// complex code path.
35+
if err := checkTerraformVersion(t, kind, *helper.TerraformVersion()); err != nil {
36+
return err
5637
}
5738

5839
configRequest := teststep.PrepareConfigurationRequest{
@@ -132,19 +113,12 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
132113
importConfig = cfgRaw
133114
}
134115

135-
// Update the test config dependent on the kind of import test being performed
136-
switch step.ImportStateKind {
137-
case ImportBlockWithResourceIdentity:
138-
t.Fatalf("TODO implement me")
139-
case ImportBlockWithId:
140-
importConfig += fmt.Sprintf(`
141-
import {
142-
to = %s
143-
id = %q
116+
if kind.plannable() {
117+
if kind.resourceIdentity() {
118+
importConfig = appendImportWithResourceIDBlock(importConfig, resourceName, importId)
119+
} else {
120+
importConfig = appendImportWithIDBlock(importConfig, resourceName, importId)
144121
}
145-
`, step.ResourceName, importId)
146-
default:
147-
// Not an import block test so nothing to do here
148122
}
149123

150124
confRequest := teststep.PrepareConfigurationRequest{
@@ -426,3 +400,104 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
426400

427401
return nil
428402
}
403+
404+
func requireNoopResourceAction(ctx context.Context, t testing.T, plan *tfjson.Plan, resourceName string, importWd *plugintest.WorkingDir, providers *providerFactories) error {
405+
t.Helper()
406+
407+
rc := findResourceChangeInPlan(t, plan, resourceName)
408+
if rc == nil || rc.Change == nil || rc.Change.Actions == nil {
409+
// does this matter?
410+
return nil
411+
}
412+
413+
// should this be length checked and used as a condition, if it's a no-op then there shouldn't be any other changes here
414+
for _, action := range rc.Change.Actions {
415+
if action != "no-op" {
416+
var stdout string
417+
err := runProviderCommand(ctx, t, func() error {
418+
var err error
419+
stdout, err = importWd.SavedPlanRawStdout(ctx)
420+
return err
421+
}, importWd, providers)
422+
if err != nil {
423+
return fmt.Errorf("retrieving formatted plan output: %w", err)
424+
}
425+
426+
return fmt.Errorf("importing resource %s: expected a no-op resource action, got %q action with plan \nstdout:\n\n%s", rc.Address, action, stdout)
427+
}
428+
}
429+
430+
return nil
431+
}
432+
433+
func findResourceChangeInPlan(t testing.T, plan *tfjson.Plan, resourceName string) *tfjson.ResourceChange {
434+
t.Helper()
435+
436+
for _, rc := range plan.ResourceChanges {
437+
if rc.Address == resourceName {
438+
return rc
439+
}
440+
}
441+
return nil
442+
}
443+
444+
func appendImportWithIDBlock(config string, resourceName string, importID string) string {
445+
return config + fmt.Sprintf(``+"\n"+
446+
`import {`+"\n"+
447+
` to = %s`+"\n"+
448+
` id = %q`+"\n"+
449+
`}`,
450+
resourceName, importID)
451+
}
452+
453+
func appendImportWithResourceIDBlock(config string, resourceName string, importID string) string {
454+
return config + fmt.Sprintf(``+"\n"+
455+
`import {`+"\n"+
456+
` to = %s`+"\n"+
457+
` identity = {`+"\n"+
458+
` // Add identity attributes here`+"\n"+
459+
` }`+"\n"+
460+
`}`+"\n",
461+
resourceName) //, importID)
462+
}
463+
464+
func checkTerraformVersion(t testing.T, kind ImportStateKind, versionUnderTest version.Version) error {
465+
t.Helper()
466+
467+
if versionUnderTest.Core().LessThan(kind.terraformVersion()) {
468+
return fmt.Errorf(
469+
`%s steps require Terraform %s. Detected Terraform %s. Either upgrade the Terraform version running the test `+
470+
`or add `+"`TerraformVersionChecks`"+` to the test case to skip this test.`+"\n\n"+
471+
`https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`,
472+
kind, kind.terraformVersion(), versionUnderTest)
473+
}
474+
475+
return nil
476+
}
477+
478+
func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) {
479+
t.Helper()
480+
481+
var states []*terraform.InstanceState
482+
for address, r := range importState.RootModule().Resources {
483+
if strings.HasPrefix(address, "data.") {
484+
continue
485+
}
486+
487+
if r.Primary == nil {
488+
continue
489+
}
490+
491+
is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage
492+
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
493+
states = append(states, is)
494+
}
495+
496+
logging.HelperResourceTrace(ctx, "Calling TestStep ImportStateCheck")
497+
498+
if err := step.ImportStateCheck(states); err != nil {
499+
t.Fatal(err)
500+
}
501+
502+
logging.HelperResourceTrace(ctx, "Called TestStep ImportStateCheck")
503+
}

0 commit comments

Comments
 (0)