Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a0ef42
Add testdata for a writeable config directory
bbasata Apr 28, 2025
6e8a7ed
Add testdata for a writeable config file
bbasata Apr 28, 2025
85c46da
Add TestStep flag to opt out of config modification
bbasata Apr 28, 2025
21e8d54
A failing test
bbasata Apr 28, 2025
c1aafe4
Prepare to add a field to configDirectory
bbasata Apr 28, 2025
748d9d5
Accept optional generated config for a config directory
bbasata Apr 28, 2025
c53eb24
Write generated import config to ConfigDirectory
bbasata Apr 28, 2025
f4c6492
Rename to Append()
bbasata Apr 30, 2025
cdc70c1
Refactor to Append()
bbasata Apr 30, 2025
2ef656a
Refactor to append() style
bbasata Apr 30, 2025
07b1779
Rename to step.ConfigExact
bbasata May 2, 2025
1dae6f8
lint
bbasata May 2, 2025
b5f05da
Extend ConfigExact to Config and ConfigFile
bbasata May 2, 2025
0172dec
Merge branch 'main' into flexible-config-directory
bbasata May 2, 2025
52cc5ee
fixup! Extend ConfigExact to Config and ConfigFile
bbasata May 2, 2025
d065ac4
Tidy importstate/testdata
bbasata May 2, 2025
75caa28
tidy
bbasata May 2, 2025
2f36714
tidy
bbasata May 2, 2025
09fd5f0
tidy
bbasata May 2, 2025
80943fc
tidy
bbasata May 2, 2025
de0a1a0
tidy
bbasata May 2, 2025
507b3b2
tidy
bbasata May 2, 2025
1d4d6b7
Add a test for appending to ConfigFile
bbasata May 5, 2025
ba89faf
Fix testdata
bbasata May 6, 2025
5f9a0d0
Add changelog entry
bbasata May 6, 2025
956450d
copywrite
bbasata May 6, 2025
b74081a
Update test expectation
bbasata May 6, 2025
712b939
Rename to ImportStateConfigExact and tidy comment
bbasata May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/NOTES-20250506-121618.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: NOTES
body: 'ImportState: extend auto-generated import blocks to work with `ConfigFile` and `ConfigDirectory`; adds `ConfigExact` flag to opt out'
time: 2025-05-06T12:16:18.375025-04:00
custom:
Issue: "494"
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestImportBlock_AsFirstStep(t *testing.T) {
id = "westeurope/somevalue"
}
`,
ImportStateConfigExact: true,
ImportPlanChecks: r.ImportPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,35 @@ func TestImportBlock_InConfigDirectory(t *testing.T) {
},
})
}

func TestImportBlock_InConfigDirectory_ConfigExactTrue(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
Resources: map[string]testprovider.Resource{
"examplecloud_container": examplecloudResource(),
},
}),
},
Steps: []r.TestStep{
{
ConfigDirectory: config.StaticDirectory(`testdata/1`),
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,

// This content includes an import block with an ID so we will
// use the exact content
ConfigDirectory: config.StaticDirectory(`testdata/2_with_exact_import_config`),
ImportStateConfigExact: true,
},
},
})
}
36 changes: 34 additions & 2 deletions helper/resource/importstate/import_block_in_config_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestImportBlock_InConfigFile(t *testing.T) {
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container_import.tf`),
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container.tf`),
},
},
})
Expand Down Expand Up @@ -66,7 +66,39 @@ func TestImportBlock_WithResourceIdentity_InConfigFile(t *testing.T) {
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithResourceIdentity,
ConfigFile: config.StaticFile(`testdata/examplecloud_container_import_with_identity.tf`),
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container.tf`),
},
},
})
}

func TestImportBlock_InConfigFile_ConfigExactTrue(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
Resources: map[string]testprovider.Resource{
"examplecloud_container": examplecloudResource(),
},
}),
},
Steps: []r.TestStep{
{
ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`),
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,

// This content includes an import block with an ID so we will
// use the exact content
ConfigFile: config.StaticFile(`testdata/examplecloud_container_with_exact_import_config_with_id.tf`),
ImportStateConfigExact: true,
},
},
})
Expand Down
25 changes: 14 additions & 11 deletions helper/resource/importstate/import_block_with_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ func TestImportBlock_WithID_ExpectError(t *testing.T) {
id = "westeurope/somevalue"
}
`,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op import operation, got.*\["update"\] action with plan(.?)`),
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateConfigExact: true,
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op import operation, got.*\["update"\] action with plan(.?)`),
},
},
})
Expand Down Expand Up @@ -295,9 +296,10 @@ func TestImportBlock_WithID_WithBlankOptionalAttribute_GeneratesCorrectPlan(t *t
id = "sometestid"

}`,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateConfigExact: true,
},
},
})
Expand Down Expand Up @@ -416,10 +418,11 @@ import {
Config: config,
},
{
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
Config: configWithImportBlock,
ResourceName: "random_string.mystery_message",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateConfigExact: true,
Config: configWithImportBlock,
ResourceName: "random_string.mystery_message",
ImportPlanChecks: r.ImportPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownValue(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

resource "examplecloud_container" "test" {
name = "somevalue"
location = "westeurope"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,5 @@ resource "examplecloud_container" "test" {

import {
to = examplecloud_container.test
identity = {
id = "examplecloud_container.test"
}
id = "examplecloud_container.test"
}
10 changes: 10 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,16 @@ type TestStep struct {
// otherwise an error will be returned.
ConfigFile config.TestStepConfigFunc

// ImportStateConfigExact indicates that the test framework should use the exact
// content of the Config, ConfigFile, or ConfigDirectory inputs and should
// not modify it at test run time.
//
// The default is false. At test run time, the test framework will generate
// specific kinds of configuration, such as import blocks, and append them
// to the given Config, ConfigFile, or ConfigDirectory inputs. Using this
// default improves test readability and removes duplication of setup.
ImportStateConfigExact bool

// ConfigVariables is a map defining variables for use in conjunction
// with Terraform configuration. If this map is populated then it
// will be used to assemble an *.auto.tfvars.json which will be
Expand Down
72 changes: 37 additions & 35 deletions helper/resource/testing_new_import_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,34 +105,36 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
}
}

var inlineConfig string
if step.Config != "" {
inlineConfig = step.Config
} else {
inlineConfig = cfgRaw
}
testStepConfigRequest := config.TestStepConfigRequest{
StepNumber: stepNumber,
TestName: t.Name(),
}
testStepConfig := teststep.Configuration(teststep.PrepareConfigurationRequest{
Directory: step.ConfigDirectory,
File: step.ConfigFile,
Raw: step.Config,
Raw: inlineConfig,
TestStepConfigRequest: testStepConfigRequest,
}.Exec())

if testStepConfig == nil {
logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import")
importConfig := cfgRaw
switch {
case step.ImportStateConfigExact:
break

if kind.plannable() && kind.resourceIdentity() {
importConfig = appendImportBlockWithIdentity(importConfig, resourceName, priorIdentityValues)
} else if kind.plannable() {
importConfig = appendImportBlock(importConfig, resourceName, importId)
}
case kind.plannable() && kind.resourceIdentity():
testStepConfig = appendImportBlockWithIdentity(testStepConfig, resourceName, priorIdentityValues)

testStepConfig = teststep.Configuration(teststep.PrepareConfigurationRequest{
Raw: importConfig,
TestStepConfigRequest: testStepConfigRequest,
}.Exec())
if testStepConfig == nil {
t.Fatal("Cannot import state with no specified config")
}
case kind.plannable():
testStepConfig = appendImportBlock(testStepConfig, resourceName, importId)
}

if testStepConfig == nil {
t.Fatal("Cannot import state with no specified config")
}

var workingDir *plugintest.WorkingDir
Expand Down Expand Up @@ -424,51 +426,51 @@ func testImportCommand(ctx context.Context, t testing.T, workingDir *plugintest.
return nil
}

func appendImportBlock(config string, resourceName string, importID string) string {
return config + fmt.Sprintf(``+"\n"+
`import {`+"\n"+
` to = %s`+"\n"+
` id = %q`+"\n"+
`}`,
resourceName, importID)
func appendImportBlock(config teststep.Config, resourceName string, importID string) teststep.Config {
return config.Append(
fmt.Sprintf(``+"\n"+
`import {`+"\n"+
` to = %s`+"\n"+
` id = %q`+"\n"+
`}`,
resourceName, importID))
}

func appendImportBlockWithIdentity(config string, resourceName string, identityValues map[string]any) string {
configBuilder := config
configBuilder += fmt.Sprintf(``+"\n"+
func appendImportBlockWithIdentity(config teststep.Config, resourceName string, identityValues map[string]any) teststep.Config {
configBuilder := strings.Builder{}
configBuilder.WriteString(fmt.Sprintf(``+"\n"+
`import {`+"\n"+
` to = %s`+"\n"+
` identity = {`+"\n",
resourceName)
resourceName))

for k, v := range identityValues {
switch v := v.(type) {
case bool:
configBuilder += fmt.Sprintf(` %q = %t`+"\n", k, v)
configBuilder.WriteString(fmt.Sprintf(` %q = %t`+"\n", k, v))

case []any:
var quotedV []string
for _, v := range v {
quotedV = append(quotedV, fmt.Sprintf(`%q`, v))
}
configBuilder += fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", "))
configBuilder.WriteString(fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", ")))

case json.Number:
configBuilder += fmt.Sprintf(` %q = %s`+"\n", k, v)
configBuilder.WriteString(fmt.Sprintf(` %q = %s`+"\n", k, v))

case string:
configBuilder += fmt.Sprintf(` %q = %q`+"\n", k, v)
configBuilder.WriteString(fmt.Sprintf(` %q = %q`+"\n", k, v))

default:
panic(fmt.Sprintf("unexpected type %T for identity value %q", v, k))
}
}

configBuilder += `` +
` }` + "\n" +
`}` + "\n"
configBuilder.WriteString(` }` + "\n")
configBuilder.WriteString(`}` + "\n")

return configBuilder
return config.Append(configBuilder.String())
}

func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error {
Expand Down
27 changes: 22 additions & 5 deletions internal/teststep/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Config interface {
HasProviderBlock(context.Context) (bool, error)
HasTerraformBlock(context.Context) (bool, error)
Write(context.Context, string) error
Append(string) Config
}

// PrepareConfigurationRequest is used to simplify the generation of
Expand Down Expand Up @@ -151,7 +152,7 @@ func copyFiles(path string, dstPath string) error {
if info.IsDir() {
continue
} else {
err = copyFile(srcPath, dstPath)
_, err = copyFile(srcPath, dstPath)

if err != nil {
return err
Expand All @@ -164,19 +165,19 @@ func copyFiles(path string, dstPath string) error {

// copyFile accepts a path to a file and a destination,
// copying the file from path to destination.
func copyFile(path string, dstPath string) error {
func copyFile(path string, dstPath string) (string, error) {
srcF, err := os.Open(path)

if err != nil {
return err
return "", err
}

defer srcF.Close()

di, err := os.Stat(dstPath)

if err != nil {
return err
return "", err
}

if di.IsDir() {
Expand All @@ -187,12 +188,28 @@ func copyFile(path string, dstPath string) error {
dstF, err := os.Create(dstPath)

if err != nil {
return err
return "", err
}

defer dstF.Close()

if _, err := io.Copy(dstF, srcF); err != nil {
return "", err
}

return dstPath, nil
}

// appendToFile accepts a path to a file and a string,
// appending the file from path to destination.
func appendToFile(path string, content string) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
return err
}
defer f.Close()

if _, err := io.WriteString(f, content); err != nil {
return err
}

Expand Down
Loading
Loading