mirror of
https://github.com/hashicorp/terraform.git
synced 2026-05-28 04:03:27 -04:00
PSS: Add parsing of .tfmigrate.hcl files to define state migration operations (#38526)
* feat: Parsing a configuration directory can include .tfmigrate.hcl files. These files include `state_store_provider` blocks and `from` blocks that contains either a single `backend` or `state_store` block. Validation will enforce that despite multiple .tfmigrate.hcl files being parsed you can only have one state_store_provider or from block, and nested backend/state_store blocks are mutually exclusive. Use of state_store_provider is only valid when state_store is in use, and the single provider described in both blocks must be in agreement. Also, a directory's .tfmigrate.hcl files cannot be empty once combined.
This commit is contained in:
parent
008c92d91f
commit
634db2dcc8
27 changed files with 1016 additions and 25 deletions
|
|
@ -40,6 +40,8 @@ type Module struct {
|
|||
ProviderLocalNames map[addrs.Provider]string
|
||||
ProviderMetas map[addrs.Provider]*ProviderMeta
|
||||
|
||||
StateMigrationInstructions *StateMigrationInstructions
|
||||
|
||||
Variables map[string]*Variable
|
||||
Locals map[string]*Local
|
||||
Outputs map[string]*Output
|
||||
|
|
@ -106,9 +108,7 @@ type File struct {
|
|||
// test files.
|
||||
func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[string]*TestFile) (*Module, hcl.Diagnostics) {
|
||||
mod, diags := NewModule(primaryFiles, overrideFiles)
|
||||
if mod != nil {
|
||||
mod.Tests = testFiles
|
||||
}
|
||||
mod.Tests = testFiles
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
|
|
@ -650,6 +650,59 @@ func (m *Module) appendQueryFile(file *QueryFile) hcl.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
// appendStateMigrationFile controls how multiple .tfmigrate.hcl files are combined
|
||||
// to result in the final state migration configuration. This enables multiple blocks
|
||||
// to be defined across multiple files.
|
||||
func (m *Module) appendStateMigrationFile(file *StateMigrationFile) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
// Validate process of combining data from across multiple files.
|
||||
// This includes identifying duplications or conflicts across files.
|
||||
// Note: Validation of individual files should have happened earlier when they were parsed.
|
||||
if file.StateStoreProvider != nil {
|
||||
if m.StateMigrationInstructions.StateStoreProvider == nil {
|
||||
m.StateMigrationInstructions.StateStoreProvider = file.StateStoreProvider
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "state_store_provider" configuration block`,
|
||||
Detail: fmt.Sprintf(`A "state_store_provider" block was already declared at %s. Only one of these blocks can be included in a module's state migration files.`, m.StateMigrationInstructions.StateStoreProvider.DeclRange),
|
||||
Subject: &file.StateStoreProvider.DeclRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
if file.StateStore != nil {
|
||||
if m.StateMigrationInstructions.StateStore == nil {
|
||||
m.StateMigrationInstructions.StateStore = file.StateStore
|
||||
} else {
|
||||
// If we're encountering a duplicate 'state_store' description it means that a duplicate
|
||||
// 'from' block is present, so we report it as such.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "from" configuration block`,
|
||||
Detail: `Only one "from" block is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: file.fromBlockSource,
|
||||
})
|
||||
}
|
||||
}
|
||||
if file.Backend != nil {
|
||||
if m.StateMigrationInstructions.Backend == nil {
|
||||
m.StateMigrationInstructions.Backend = file.Backend
|
||||
} else {
|
||||
// If we're encountering a duplicate 'backend' description it means that a duplicate
|
||||
// 'from' block is present, so we report it as such.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "from" configuration block`,
|
||||
Detail: `Only one "from" block is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: file.fromBlockSource,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,17 @@ func (p *Parser) LoadQueryFile(path string) (*QueryFile, hcl.Diagnostics) {
|
|||
return query, diags
|
||||
}
|
||||
|
||||
func (p *Parser) LoadStateMigrationFile(path string) (*StateMigrationFile, hcl.Diagnostics) {
|
||||
body, diags := p.LoadHCLFile(path)
|
||||
if body == nil {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
stateMigrations, stateMigrationsDiags := loadStateMigrationFile(body)
|
||||
diags = diags.Extend(stateMigrationsDiags)
|
||||
return stateMigrations, diags
|
||||
}
|
||||
|
||||
// LoadMockDataFile reads the file at the given path and parses it as a
|
||||
// Terraform mock data file.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const (
|
|||
// MatchTestFiles option, or from the default test directory.
|
||||
// If this option is not specified, test files will not be loaded.
|
||||
// Query files (.tfquery.hcl) are also loaded from the given directory.
|
||||
// State Migration files (.tfmigrate.hcl) are also loaded from the given directory.
|
||||
//
|
||||
// If this method returns nil, that indicates that the given directory does not
|
||||
// exist at all or could not be opened for some reason. Callers may wish to
|
||||
|
|
@ -59,30 +60,110 @@ func (p *Parser) LoadConfigDir(path string, opts ...Option) (*Module, hcl.Diagno
|
|||
|
||||
// Initialize the module
|
||||
mod, modDiags := NewModule(primary, override)
|
||||
mod.SourceDir = path
|
||||
diags = diags.Extend(modDiags)
|
||||
|
||||
// Check if we need to load test files
|
||||
if len(fileSet.Tests) > 0 {
|
||||
testFiles, fDiags := p.loadTestFiles(path, fileSet.Tests)
|
||||
diags = diags.Extend(fDiags)
|
||||
if mod != nil {
|
||||
mod.Tests = testFiles
|
||||
}
|
||||
mod.Tests = testFiles
|
||||
}
|
||||
// Check if we need to load query files
|
||||
if len(fileSet.Queries) > 0 {
|
||||
queryFiles, fDiags := p.loadQueryFiles(fileSet.Queries)
|
||||
diags = append(diags, fDiags...)
|
||||
if mod != nil {
|
||||
for _, qf := range queryFiles {
|
||||
diags = diags.Extend(mod.appendQueryFile(qf))
|
||||
for _, qf := range queryFiles {
|
||||
diags = diags.Extend(mod.appendQueryFile(qf))
|
||||
}
|
||||
}
|
||||
// Check if we need to load state migration files
|
||||
if len(fileSet.StateMigrations) > 0 {
|
||||
stateMigrationFiles, fDiags := p.loadStateMigrateFiles(path, fileSet.StateMigrations)
|
||||
diags = append(diags, fDiags...)
|
||||
// If there are errors they may be duplicated below, so return early.
|
||||
// We return an incomplete module representation.
|
||||
if diags.HasErrors() {
|
||||
mod.SourceDir = path
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
mod.StateMigrationInstructions = &StateMigrationInstructions{}
|
||||
for _, smf := range stateMigrationFiles {
|
||||
diags = diags.Extend(mod.appendStateMigrationFile(smf))
|
||||
}
|
||||
|
||||
// If there are errors that might raise false positive below, so return early.
|
||||
// We return an incomplete module representation.
|
||||
if diags.HasErrors() {
|
||||
mod.SourceDir = path
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
// Now, we perform some final checks that can only be done once all .tfmigrate.hcl files are loaded.
|
||||
// Note: Other checks, like mutual exclusivity, were already performed when parsing single files or appending files.
|
||||
ssp := mod.StateMigrationInstructions.StateStoreProvider
|
||||
ss := mod.StateMigrationInstructions.StateStore
|
||||
b := mod.StateMigrationInstructions.Backend
|
||||
switch {
|
||||
case ssp == nil && ss == nil && b == nil:
|
||||
// Files present but all empty
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Empty state migration configuration`,
|
||||
Detail: `The configuration includes .tfmigrate.hcl files, but they are empty. Please make sure they include the necessary blocks to define a state migration, or remove the files from your project.`,
|
||||
})
|
||||
case ss != nil && b != nil:
|
||||
// Mutually exclusive 'from { backend }' and 'from { state_store }' both present
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "backend" and "state_store"`,
|
||||
Detail: `A configuration cannot include both "backend" and "state_store" blocks. Remove one of these blocks from inside the "from" block. The remaining block should describe where your existing state should be migrated from.`,
|
||||
// Sourceless because we don't know which block isn't needed.
|
||||
})
|
||||
case ssp != nil && b != nil:
|
||||
// Mutually exclusive 'from { backend }' and 'state_store_provider' both present
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "backend" and "state_store_provider"`,
|
||||
Detail: `The "state_store_provider" block can only be used in combination with a "state_store" block. Either remove the unused "state_store_provider" block, or replace the "backend" block with a "state_store" block.`,
|
||||
// Blame the state_store_provider block as the problem, as this case will only be evaluated if
|
||||
// there isn't a migrate_from_state_store block also present.
|
||||
Subject: &ssp.DeclRange,
|
||||
})
|
||||
case ss != nil && ssp == nil:
|
||||
// Missing 'state_store_provider' block
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Missing "state_store_provider" block for state store migration`,
|
||||
Detail: `The configuration includes a "state_store" block but is missing the required "state_store_provider" block. Add a "state_store_provider" block to specify the provider to use when migrating state out of that state store.`,
|
||||
})
|
||||
case ss == nil && ssp != nil:
|
||||
// Missing 'from { state_store }' block
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Missing "state_store" block for state store migration`,
|
||||
Detail: `The configuration includes a "state_store_provider" block but is missing the required "state_store" block. Add a "state_store" block, nested in a "from" block, to specify the state store to migrate from.`,
|
||||
})
|
||||
case ss != nil && ssp != nil:
|
||||
// Both 'from { state_store }' and 'state_store_provider' blocks are present,
|
||||
// but are they in agreement with each other?
|
||||
if ss.Provider.Name != ssp.Name {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Inconsistent provider information for state migration`,
|
||||
Detail: fmt.Sprintf(`The configuration's "state_store_provider" block defines a provider called %q but the "migrate_from_state_store" block uses a provider called %q instead. Please update the blocks so that they are in agreement.`,
|
||||
ssp.Name,
|
||||
ss.Provider.Name,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
// They match, so copy across relevant data.
|
||||
ss.ProviderAddr = ssp.Type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mod != nil {
|
||||
mod.SourceDir = path
|
||||
}
|
||||
mod.SourceDir = path
|
||||
|
||||
return mod, diags
|
||||
}
|
||||
|
|
@ -220,6 +301,19 @@ func (p *Parser) loadQueryFiles(paths []string) ([]*QueryFile, hcl.Diagnostics)
|
|||
return files, diags
|
||||
}
|
||||
|
||||
func (p *Parser) loadStateMigrateFiles(basePath string, paths []string) ([]*StateMigrationFile, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
files := make([]*StateMigrationFile, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
f, fDiags := p.LoadStateMigrationFile(path)
|
||||
diags = append(diags, fDiags...)
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
return files, diags
|
||||
}
|
||||
|
||||
// fileExt returns the Terraform configuration extension of the given
|
||||
// path, or a blank string if it is not a recognized extension.
|
||||
func fileExt(path string) string {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
)
|
||||
|
||||
// TestParseLoadConfigDirSuccess is a simple test that just verifies that
|
||||
|
|
@ -120,7 +122,6 @@ func TestParserLoadConfigDirSuccess(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParserLoadConfigDirWithTests(t *testing.T) {
|
||||
|
|
@ -137,7 +138,6 @@ func TestParserLoadConfigDirWithTests(t *testing.T) {
|
|||
|
||||
for _, directory := range directories {
|
||||
t.Run(directory, func(t *testing.T) {
|
||||
|
||||
testDirectory := DefaultTestDirectory
|
||||
if directory == "testdata/valid-modules/with-tests-very-nested" {
|
||||
testDirectory = "very/nested"
|
||||
|
|
@ -238,8 +238,212 @@ func TestParserLoadConfigDirWithQueries(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParserLoadTestFiles_Invalid(t *testing.T) {
|
||||
// Testing happy path use of 'from { backend }'.
|
||||
func TestParserLoadConfigDirWithStateMigrations_from_backend(t *testing.T) {
|
||||
testFixtures := "testdata/state-migration-files/valid/migration-from-backend"
|
||||
// Below are specified in the config above
|
||||
backendType := "s3"
|
||||
bucketName := "foobar"
|
||||
|
||||
// Parse the directory, including .tfmigrate.hcl files
|
||||
parser := NewParser(nil)
|
||||
mod, diags := parser.LoadConfigDir(testFixtures, MatchStateMigrateFiles())
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors: %s", diags)
|
||||
}
|
||||
if mod.StateMigrationInstructions == nil || mod.StateMigrationInstructions.Backend == nil {
|
||||
t.Fatalf("expected mod.StateMigrationInstructions.MigrateFromBackend to be initialized, got:\n mod.StateMigrationInstructions = %#v\n mod.StateMigrationInstructions.MigrateFromBackend = %#v",
|
||||
mod.StateMigrationInstructions,
|
||||
mod.StateMigrationInstructions.Backend,
|
||||
)
|
||||
}
|
||||
|
||||
// Assert that the module includes expected information from 'from { backend }' block
|
||||
b := mod.StateMigrationInstructions.Backend
|
||||
if b.Type != backendType {
|
||||
t.Fatalf("wrong backend type, got %q, want %q", b.Type, backendType)
|
||||
}
|
||||
attributes, diags := b.Config.JustAttributes()
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error inspecting backend config: %s", diags)
|
||||
}
|
||||
gotBucketName, diags := attributes["bucket"].Expr.Value(nil)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error inspecting bucket attribute: %s", diags)
|
||||
}
|
||||
if gotBucketName.AsString() != bucketName {
|
||||
t.Fatalf("wrong bucket name, got %q, want %q", gotBucketName, bucketName)
|
||||
}
|
||||
}
|
||||
|
||||
// Testing happy path use of 'from { state_store }'. This requires use of the state_store_provider
|
||||
// block as well, so this also checks the happy path for that block.
|
||||
func TestParserLoadConfigDirWithStateMigrations_from_state_store(t *testing.T) {
|
||||
testFixtures := "testdata/state-migration-files/valid/migration-from-state-store"
|
||||
// Below are specified in the config above
|
||||
stateStoreType := "test_store"
|
||||
|
||||
// Parse the directory, including .tfmigrate.hcl files
|
||||
parser := NewParser(nil)
|
||||
mod, diags := parser.LoadConfigDir(testFixtures, MatchStateMigrateFiles())
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors: %s", diags)
|
||||
}
|
||||
if mod.StateMigrationInstructions == nil || mod.StateMigrationInstructions.StateStore == nil || mod.StateMigrationInstructions.StateStoreProvider == nil {
|
||||
t.Fatalf("expected MigrateFromStateStore and StateStoreProvider to be initialized, got:\n mod.StateMigrationInstructions = %#v\n mod.StateMigrationInstructions.MigrateFromStateStore = %#v\n mod.StateMigrationInstructions.StateStoreProvider = %#v",
|
||||
mod.StateMigrationInstructions,
|
||||
mod.StateMigrationInstructions.StateStore,
|
||||
mod.StateMigrationInstructions.StateStoreProvider,
|
||||
)
|
||||
}
|
||||
|
||||
// Assert that the module includes expected information from 'from { state_store }' block
|
||||
ss := mod.StateMigrationInstructions.StateStore
|
||||
if ss.Type != stateStoreType {
|
||||
t.Fatalf("wrong state store type, got %q, want %q", ss.Type, stateStoreType)
|
||||
}
|
||||
if ss.Config == nil {
|
||||
t.Fatalf("expected config to be non-nil")
|
||||
}
|
||||
if !ss.ProviderAddr.Equals(mod.StateMigrationInstructions.StateStoreProvider.Type) {
|
||||
t.Fatalf("expected state store description's provider addr to have been populated with %q, but got %q", mod.StateMigrationInstructions.StateStoreProvider.Type.ForDisplay(), ss.ProviderAddr.ForDisplay())
|
||||
}
|
||||
if ss.ProviderSupplyMode != "" {
|
||||
// This is expected to be populated by calling code
|
||||
// that is reading the config, not by the parser itself.
|
||||
t.Fatal("unexpected data in ProviderSupplyMode")
|
||||
}
|
||||
|
||||
// Assert that the module includes expected information from state_store_provider block
|
||||
ssp := mod.StateMigrationInstructions.StateStoreProvider
|
||||
if ssp.Name != "test" || ssp.Source != "hashicorp/test" || !ssp.Type.Equals(addrs.NewDefaultProvider("test")) {
|
||||
t.Fatalf("unexpected state store provider info, got:\n Name: %q\n Source: %q\n Type: %q\n VersionConstraint: %q",
|
||||
ssp.Name, ssp.Source, ssp.Type, ssp.Requirement,
|
||||
)
|
||||
}
|
||||
expectedConstraint := "1.0.0"
|
||||
if ssp.Requirement.Required.String() != expectedConstraint {
|
||||
t.Fatalf("unexpected version constraint, got %q, want %q", ssp.Requirement.Required.String(), expectedConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserLoadConfigDirWithStateMigrations_error_cases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
directory string
|
||||
diagnosticSummary string
|
||||
source string
|
||||
}{
|
||||
// Duplicated blocks
|
||||
{
|
||||
name: "duplicated 'from' block",
|
||||
directory: "testdata/state-migration-files/invalid/duplicate-from-block-same-file",
|
||||
diagnosticSummary: "Duplicate \"from\" configuration block",
|
||||
// Assert the source because we reference the second parsed 'from' block
|
||||
source: "1-file.tfmigrate.hcl:17,1-5",
|
||||
},
|
||||
{
|
||||
name: "duplicated 'from' block across multiple files",
|
||||
directory: "testdata/state-migration-files/invalid/duplicate-from-block-multiple-files",
|
||||
diagnosticSummary: "Duplicate \"from\" configuration block",
|
||||
// Assert the source because we reference the 'from' block in the second parsed file
|
||||
source: "2-file.tfmigrate.hcl:1,1-5",
|
||||
},
|
||||
{
|
||||
name: "duplicate 'backend' block in 'from' block",
|
||||
directory: "testdata/state-migration-files/invalid/duplicate-nested-backend-block",
|
||||
diagnosticSummary: "Duplicate \"backend\" configuration block",
|
||||
},
|
||||
{
|
||||
name: "duplicate 'state_store' block in 'from' block",
|
||||
directory: "testdata/state-migration-files/invalid/duplicate-nested-state-store-block",
|
||||
diagnosticSummary: "Duplicate \"state_store\" configuration block",
|
||||
},
|
||||
// Mutually exclusive blocks
|
||||
{
|
||||
name: "backend and state_store are mutually exclusive in same 'from' block",
|
||||
directory: "testdata/state-migration-files/invalid/both-nested-state-store-and-backend-blocks",
|
||||
diagnosticSummary: `Invalid combination of "backend" and "state_store"`,
|
||||
// Assert the source because we reference the 'from' block as incorrect, instead of one of the nested blocks
|
||||
source: "main.tfmigrate.hcl:4,1-5",
|
||||
},
|
||||
{
|
||||
name: "backend and state_store_provider are mutually exclusive",
|
||||
directory: "testdata/state-migration-files/invalid/backend-and-state-store-provider-same-file",
|
||||
diagnosticSummary: `Invalid combination of "backend" and "state_store_provider"`,
|
||||
},
|
||||
{
|
||||
name: "backend and state_store_provider are mutually exclusive across multiple files",
|
||||
directory: "testdata/state-migration-files/invalid/backend-and-state-store-provider-multiple-files",
|
||||
diagnosticSummary: `Invalid combination of "backend" and "state_store_provider"`,
|
||||
},
|
||||
// Missing blocks
|
||||
{
|
||||
name: "only state_store_provider block, missing state_store",
|
||||
directory: "testdata/state-migration-files/invalid/only-state-store-provider-block",
|
||||
diagnosticSummary: `Missing "state_store" block for state store migration`,
|
||||
},
|
||||
{
|
||||
name: "only state_store block, missing state_store_provider",
|
||||
directory: "testdata/state-migration-files/invalid/only-state-store-block",
|
||||
diagnosticSummary: `Missing "state_store_provider" block for state store migration`,
|
||||
},
|
||||
{
|
||||
name: "no blocks present in the files",
|
||||
directory: "testdata/state-migration-files/invalid/no-blocks",
|
||||
diagnosticSummary: `Empty state migration configuration`,
|
||||
},
|
||||
// Invalid contents of state_store_provider block
|
||||
{
|
||||
name: "invalid version constraint in state_store_provider block",
|
||||
directory: "testdata/state-migration-files/invalid/invalid-version-state-store-provider-block",
|
||||
diagnosticSummary: `Invalid provider version in "state_store_provider" configuration block`,
|
||||
},
|
||||
{
|
||||
name: "unexpected attribute in state_store_provider block",
|
||||
directory: "testdata/state-migration-files/invalid/unexpected-attribute-state-store-provider-block",
|
||||
diagnosticSummary: `Invalid state_store_provider object; state_store_provider objects can only contain "version" and "source" attributes.`,
|
||||
},
|
||||
{
|
||||
name: "different providers in migrate_from_state_store and state_store_provider blocks",
|
||||
directory: "testdata/state-migration-files/invalid/different-providers-between-blocks",
|
||||
diagnosticSummary: `Inconsistent provider information for state migration`,
|
||||
},
|
||||
{
|
||||
name: "multiple providers described in a state_store_provider block",
|
||||
directory: "testdata/state-migration-files/invalid/multiple-providers-in-state-store-provider-block",
|
||||
diagnosticSummary: `Unexpected number of providers described in "state_store_provider" configuration block.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
parser := NewParser(nil)
|
||||
_, diags := parser.LoadConfigDir(test.directory, MatchStateMigrateFiles())
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("expected errors but got none: %s", diags)
|
||||
}
|
||||
if len(diags) != 1 {
|
||||
for _, diag := range diags {
|
||||
t.Log(diag)
|
||||
}
|
||||
t.Fatalf("expected only a single diagnostic to be returned, but got %d: \n%#v", len(diags), diags)
|
||||
}
|
||||
if !strings.Contains(diags.Error(), test.diagnosticSummary) {
|
||||
t.Fatalf("expected error to contain %q, but got %q", test.diagnosticSummary, diags.Error())
|
||||
}
|
||||
if test.source != "" {
|
||||
// We're only asserting source content in cases where the fromBlockSource value is used.
|
||||
expectedSource := path.Join(test.directory, test.source)
|
||||
if diags[0].Subject.String() != expectedSource {
|
||||
t.Fatalf("expected error subject to be %q, but got %q", expectedSource, diags[0].Subject.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserLoadTestFiles_Invalid(t *testing.T) {
|
||||
tcs := map[string][]string{
|
||||
"duplicate_data_overrides": {
|
||||
"duplicate_data_overrides.tftest.hcl:7,3-16: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:2,3-16.",
|
||||
|
|
@ -424,7 +628,6 @@ func TestParserLoadConfigDirFailure(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsEmptyDir(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ import (
|
|||
|
||||
// ConfigFileSet holds the different types of configuration files found in a directory.
|
||||
type ConfigFileSet struct {
|
||||
Primary []string // Regular .tf and .tf.json files
|
||||
Override []string // Override files (override.tf or *_override.tf)
|
||||
Tests []string // Test files (.tftest.hcl or .tftest.json)
|
||||
Queries []string // Query files (.tfquery.hcl)
|
||||
Primary []string // Regular .tf and .tf.json files
|
||||
Override []string // Override files (override.tf or *_override.tf)
|
||||
Tests []string // Test files (.tftest.hcl or .tftest.json)
|
||||
Queries []string // Query files (.tfquery.hcl)
|
||||
StateMigrations []string // State migration files (.tfmigrate.hcl)
|
||||
}
|
||||
|
||||
// FileMatcher is an interface for components that can match and process specific file types
|
||||
|
|
@ -51,10 +52,11 @@ type parserConfig struct {
|
|||
func (p *Parser) dirFileSet(dir string, opts ...Option) (ConfigFileSet, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
fileSet := ConfigFileSet{
|
||||
Primary: []string{},
|
||||
Override: []string{},
|
||||
Tests: []string{},
|
||||
Queries: []string{},
|
||||
Primary: []string{},
|
||||
Override: []string{},
|
||||
Tests: []string{},
|
||||
Queries: []string{},
|
||||
StateMigrations: []string{},
|
||||
}
|
||||
|
||||
// Set up the parser configuration
|
||||
|
|
@ -122,6 +124,8 @@ func (p *Parser) rootFiles(dir string, matchers []FileMatcher, fileSet *ConfigFi
|
|||
fileSet.Tests = append(fileSet.Tests, fullPath)
|
||||
case *queryFiles:
|
||||
fileSet.Queries = append(fileSet.Queries, fullPath)
|
||||
case *stateMigrateFiles:
|
||||
fileSet.StateMigrations = append(fileSet.StateMigrations, fullPath)
|
||||
}
|
||||
break // Stop checking other matchers once a match is found
|
||||
}
|
||||
|
|
@ -146,6 +150,13 @@ func MatchQueryFiles() Option {
|
|||
}
|
||||
}
|
||||
|
||||
// MatchStateMigrateFiles adds a matcher for Terraform state migrate files (.tfmigrate.hcl only)
|
||||
func MatchStateMigrateFiles() Option {
|
||||
return func(o *parserConfig) {
|
||||
o.matchers = append(o.matchers, &stateMigrateFiles{})
|
||||
}
|
||||
}
|
||||
|
||||
// moduleFiles matches regular Terraform configuration files (.tf and .tf.json)
|
||||
type moduleFiles struct{}
|
||||
|
||||
|
|
@ -242,3 +253,17 @@ func (q *queryFiles) Matches(name string) bool {
|
|||
func (q *queryFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stateMigrateFiles matches Terraform state migrate files (.tfmigrate.hcl only)
|
||||
type stateMigrateFiles struct{}
|
||||
|
||||
var _ FileMatcher = (*stateMigrateFiles)(nil)
|
||||
|
||||
func (s *stateMigrateFiles) Matches(name string) bool {
|
||||
return strings.HasSuffix(name, ".tfmigrate.hcl")
|
||||
}
|
||||
|
||||
func (s *stateMigrateFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
|
||||
// There are no special directories for .tfmigrate.hcl files.
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
374
internal/configs/state_migrate_file.go
Normal file
374
internal/configs/state_migrate_file.go
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/apparentlymart/go-versions/versions"
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// StateMigrationInstructions represents the sum of all state migration files within a
|
||||
// configuration directory.
|
||||
//
|
||||
// A state migration file contains blocks that define how resource state has previously
|
||||
// been stored for a given project. In combination with an updated Terraform configuration,
|
||||
// the two pieces of information describe the source and destination of state that the user
|
||||
// wishes to migrate.
|
||||
//
|
||||
// When creating a StateMigrationInstructions struct, calling code must ensure that there
|
||||
// are no duplicated or mutually-exclusive pieces of information in the original file(s).
|
||||
type StateMigrationInstructions struct {
|
||||
StateStoreProvider *RequiredProvider
|
||||
StateStore *StateStore
|
||||
|
||||
Backend *Backend
|
||||
}
|
||||
|
||||
// StateMigrationFile represents a single state migration file within a configuration directory.
|
||||
// A project can include multiple files of this type, and their contents is aggregated.
|
||||
type StateMigrationFile struct {
|
||||
StateMigrationInstructions
|
||||
|
||||
// fromBlockSource is the source range of the 'from' block in the HCL file,
|
||||
// intended to be used in error diagnostics from parsing,
|
||||
// e.g. multiple from blocks across multiple files.
|
||||
fromBlockSource *hcl.Range
|
||||
}
|
||||
|
||||
func loadStateMigrationFile(body hcl.Body) (*StateMigrationFile, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
file := &StateMigrationFile{}
|
||||
|
||||
content, contentDiags := body.Content(stateMigrationFileSchema)
|
||||
diags = append(diags, contentDiags...)
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
case "state_store_provider":
|
||||
p, pDiags := decodeStateStoreProviderBlock(block)
|
||||
diags = diags.Extend(pDiags)
|
||||
|
||||
if file.StateStoreProvider != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "state_store_provider" configuration block`,
|
||||
Detail: `Only one "state_store_provider" block is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
continue // Keep file.StateStoreProvider as first parsed block in this scenario
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
file.StateStoreProvider = p
|
||||
file.fromBlockSource = &block.DefRange
|
||||
}
|
||||
case "from":
|
||||
if file.StateStore != nil || file.Backend != nil {
|
||||
// A from block has already been parsed.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "from" configuration block`,
|
||||
Detail: `Only one "from" block is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// We're parsing the first encountered 'from' block.
|
||||
// There could still be duplications within that block, which is detected by the function.
|
||||
i, fromDiags := decodeFromBlock(block)
|
||||
diags = diags.Extend(fromDiags)
|
||||
|
||||
if !fromDiags.HasErrors() {
|
||||
file.fromBlockSource = &block.DefRange
|
||||
|
||||
// Only one of the below is non-nil
|
||||
file.StateStore = i.StateStore
|
||||
file.Backend = i.Backend
|
||||
}
|
||||
|
||||
default:
|
||||
// We don't expect other block types in state migration files.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid block type",
|
||||
Detail: fmt.Sprintf("This block type is not valid within a state migration file: %s", block.Type),
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for mutually exclusive blocks, etc.
|
||||
|
||||
// Defining two conflicting sources of state for migration.
|
||||
if file.Backend != nil && file.StateStore != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "backend" and "state_store"`,
|
||||
Detail: `The "backend" and "state_store" blocks are mutually-exclusive inside a "from" block. Only one should be used in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: file.fromBlockSource, // We can blame the 'from' block as being invalid.
|
||||
})
|
||||
}
|
||||
// Unnecessary state store-related data supplied alongside description of a backend.
|
||||
if file.Backend != nil && file.StateStoreProvider != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "backend" and "state_store_provider"`,
|
||||
Detail: `The "state_store_provider" block can only be used in combination with a "state_store" block. Either remove the unused "state_store_provider" block, or update your "from" block to contain a "state_store" block instead.`,
|
||||
// No Subject because we don't know which is correct or incorrect.
|
||||
})
|
||||
}
|
||||
|
||||
return file, diags
|
||||
}
|
||||
|
||||
// decodeFromBlock decodes a 'from' block that can only contain one of 'state_store' or 'backend' blocks.
|
||||
func decodeFromBlock(block *hcl.Block) (*StateMigrationInstructions, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
fromData := StateMigrationInstructions{}
|
||||
|
||||
fromContent, fromContentDiags := block.Body.Content(fromBlockSchema)
|
||||
diags = diags.Extend(fromContentDiags)
|
||||
|
||||
for _, block := range fromContent.Blocks {
|
||||
switch block.Type {
|
||||
case "state_store":
|
||||
ss, ssDiags := decodeStateStoreBlock(block)
|
||||
diags = diags.Extend(ssDiags)
|
||||
|
||||
if fromData.StateStore != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "state_store" configuration block`,
|
||||
Detail: `Only one "state_store" block, nested in a "from" block, is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
continue // Keep fromData.MigrateFromStateStore as first parsed block in this scenario
|
||||
}
|
||||
|
||||
if ss != nil {
|
||||
fromData.StateStore = ss
|
||||
}
|
||||
case "backend":
|
||||
b, bDiags := decodeBackendBlock(block)
|
||||
diags = diags.Extend(bDiags)
|
||||
|
||||
if fromData.Backend != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Duplicate "backend" configuration block`,
|
||||
Detail: `Only one "backend" block, nested in a "from" block, is allowed in a directory's .tfmigrate.hcl files.`,
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
continue // Keep fromData.MigrateFromBackend as first parsed block in this scenario
|
||||
}
|
||||
|
||||
if b != nil {
|
||||
fromData.Backend = b
|
||||
}
|
||||
default:
|
||||
// We don't expect other block types nested inside from blocks.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid block type",
|
||||
Detail: fmt.Sprintf("This block type is not valid to be nested inside 'from' blocks within a state migration file: %s", block.Type),
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &fromData, diags
|
||||
}
|
||||
|
||||
func decodeStateStoreProviderBlock(block *hcl.Block) (*RequiredProvider, hcl.Diagnostics) {
|
||||
// state_store_provider blocks are similar to required_provider blocks but different, so we need logic
|
||||
// similar to that in decodeProviderRequirementsBlock but distinct. E.g. version constraints must be
|
||||
// exact versions, not a range. The similarity is sufficient that we can return a RequiredProvider pointer.
|
||||
|
||||
var diags hcl.Diagnostics
|
||||
attrs, hclDiags := block.Body.JustAttributes()
|
||||
diags = diags.Extend(hclDiags)
|
||||
|
||||
// Only one provider should be in the block
|
||||
localNames := slices.Collect(maps.Keys(attrs))
|
||||
if len(localNames) != 1 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Unexpected number of providers described in "state_store_provider" configuration block.`,
|
||||
Detail: fmt.Sprintf(`The "state_store_provider" block is only expected to include a single provider, but %d were found.`, len(localNames)),
|
||||
Subject: block.DefRange.Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
localName := localNames[0] // Local name
|
||||
attr := attrs[localName] // Block containing source and version info
|
||||
|
||||
// verify that the local name is already localized or produce an error.
|
||||
nameDiags := checkProviderNameNormalized(localName, attr.Expr.Range())
|
||||
if nameDiags.HasErrors() {
|
||||
diags = append(diags, nameDiags...)
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
kvs, mapDiags := hcl.ExprMap(attr.Expr)
|
||||
if mapDiags.HasErrors() {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid "state_store_provider" object`,
|
||||
Detail: "The provider described inside state_store_provider must be an object",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Process the data inside the object describing the provider
|
||||
ssProvider := RequiredProvider{
|
||||
Name: localName,
|
||||
DeclRange: attr.Range,
|
||||
}
|
||||
for _, kv := range kvs {
|
||||
key, keyDiags := kv.Key.Value(nil)
|
||||
if keyDiags.HasErrors() {
|
||||
diags = append(diags, keyDiags...)
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if key.Type() != cty.String {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid Attribute",
|
||||
Detail: fmt.Sprintf("Invalid attribute value for provider requirement described by state_store_provider block: %#v", key),
|
||||
Subject: kv.Key.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
switch key.AsString() {
|
||||
case "version":
|
||||
vc := VersionConstraint{
|
||||
DeclRange: attr.Range,
|
||||
}
|
||||
|
||||
versionString, valDiags := kv.Value.Value(nil)
|
||||
if valDiags.HasErrors() || !versionString.Type().Equals(cty.String) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid provider version in "state_store_provider" configuration block`,
|
||||
Detail: "Version must be a string, specifying a single version.",
|
||||
Subject: kv.Value.Range().Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := versions.ParseVersion(versionString.AsString())
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid provider version in "state_store_provider" configuration block`,
|
||||
Detail: "The version attribute must specify a single, specific version (e.g. \"1.0.0\") and cannot be a version constraint with an operator.",
|
||||
Subject: kv.Value.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// We ensure user input can be parsed as a version, but we need to
|
||||
// create a constraint to be part of the returned RequiredProvider struct.
|
||||
// The constraint will pin to a specific version set by the config.
|
||||
constraints, err := version.NewConstraint(v.String())
|
||||
if err != nil {
|
||||
// NewConstraint doesn't return user-friendly errors, so we'll just
|
||||
// ignore the provided error and produce our own generic one.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Unable to create version constraint from provider version`,
|
||||
Detail: fmt.Sprintf("Terraform was unable to create an 'exact' version constraint from the provided version string: %s.", v.String()),
|
||||
Subject: kv.Value.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
vc.Required = constraints
|
||||
ssProvider.Requirement = vc
|
||||
|
||||
case "source":
|
||||
source, err := kv.Value.Value(nil)
|
||||
if err != nil || !source.Type().Equals(cty.String) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid source in "state_store_provider" configuration block`,
|
||||
Detail: "Source must be specified as a string.",
|
||||
Subject: kv.Value.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
fqn, sourceDiags := addrs.ParseProviderSourceString(source.AsString())
|
||||
if sourceDiags.HasErrors() {
|
||||
hclDiags := sourceDiags.ToHCL()
|
||||
// The diagnostics from ParseProviderSourceString don't contain
|
||||
// source location information because it has no context to compute
|
||||
// them from, and so we'll add those in quickly here before we
|
||||
// return.
|
||||
for _, diag := range hclDiags {
|
||||
if diag.Subject == nil {
|
||||
diag.Subject = kv.Value.Range().Ptr()
|
||||
}
|
||||
}
|
||||
diags = append(diags, hclDiags...)
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ssProvider.Source = source.AsString()
|
||||
ssProvider.Type = fqn
|
||||
default:
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid state_store_provider object",
|
||||
Detail: `state_store_provider objects can only contain "version" and "source" attributes.`,
|
||||
Subject: kv.Key.Range().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &ssProvider, diags
|
||||
}
|
||||
|
||||
// stateMigrationFileSchema is the schema for a .tfmigrate.hcl file, for use with
|
||||
// the `state migrate` command.
|
||||
// Whereas the current Terraform config (.tf) defines the destination that state should
|
||||
// be migrated to, these files define how a backend or state store was previously configured.
|
||||
// Due to this, these files define the source where migrated state is copied from.
|
||||
var stateMigrationFileSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "state_store_provider",
|
||||
},
|
||||
{
|
||||
Type: "from",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// fromBlockSchema is the schema for 'from' blocks within .tfmigrate.hcl files.
|
||||
var fromBlockSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "state_store",
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
{
|
||||
Type: "backend",
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from {
|
||||
backend "s3" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
backend "s3" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# No state_store_provider block here as that would trigger a different error
|
||||
# i.e. it is mutually exclusive with 'backend'.
|
||||
|
||||
from {
|
||||
backend "s3" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
state_store "test_store1" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
state_store_provider {
|
||||
foobar = {
|
||||
source = "hashicorp/foobar"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
# The state store below references a different provider to the definition above
|
||||
|
||||
from {
|
||||
state_store "test_store" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store1" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from {
|
||||
state_store "test_store2" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store1" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store2" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from {
|
||||
backend "s3" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
backend "gcs" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store1" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
state_store "test_store2" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
state_store_provider {
|
||||
test1 = {
|
||||
source = "hashicorp/test1"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
state_store_provider {
|
||||
test2 = {
|
||||
source = "hashicorp/test2"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
state_store_provider {
|
||||
test1 = {
|
||||
source = "hashicorp/test1"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
state_store_provider {
|
||||
test2 = {
|
||||
source = "hashicorp/test2"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "~>1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
foobar = {
|
||||
source = "hashicorp/foobar"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
1
internal/configs/testdata/state-migration-files/invalid/no-blocks/empty.tfmigrate.hcl
vendored
Normal file
1
internal/configs/testdata/state-migration-files/invalid/no-blocks/empty.tfmigrate.hcl
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
# No blocks here!
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from {
|
||||
state_store "test_store" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
foobar = "this shouldn't be here"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from {
|
||||
backend "s3" {
|
||||
bucket = "foobar"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
state_store_provider {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
from {
|
||||
state_store "test_store" {
|
||||
provider "test" {
|
||||
provider_attr = "foobar"
|
||||
}
|
||||
store_attr = "foobar"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue