Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
91 changes: 91 additions & 0 deletions internal/configs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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
Expand Down Expand Up @@ -651,6 +653,95 @@ 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

if file == nil {
panic("appendStateMigrationFile receives a nil *StateMigrationFile pointer")
}

backendConflictStateStoreProviderDiag := &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid combination of "migrate_from_backend" and "state_store_provider"`,
Detail: `The "state_store_provider" block can only be used in combination with "migrate_from_state_store" blocks. Either remove the unused "state_store_provider" block, or replace the "migrate_from_backend" block with a "migrate_from_state_store" block.`,
// Sourceless because we don't know which block isn't needed.
}
backendConflictMigrateFromStateStoreDiag := &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid combination of "migrate_from_backend" and "migrate_from_state_store"`,
Detail: `A configuration cannot include both "migrate_from_backend" and "migrate_from_state_store" blocks. Remove one of these blocks, and the remaining block should describe where your existing state should be migrated from.`,
// Sourceless because we don't know which block isn't needed.
}

// 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 {
if m.StateMigrationInstructions.MigrateFromBackend != nil {
// Parsed a "state_store_provider" block after a "migrate_from_backend" block
diags = diags.Append(backendConflictStateStoreProviderDiag)
return diags
}

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 include in a module's state migration files.`, m.StateMigrationInstructions.StateStoreProvider.DeclRange),
Subject: &file.StateStoreProvider.DeclRange,
})
}
}
if file.MigrateFromStateStore != nil {
if m.StateMigrationInstructions.MigrateFromStateStore == nil {
if m.StateMigrationInstructions.MigrateFromBackend != nil {
// Parsed a "migrate_from_state_store" block after a "migrate_from_backend" block
diags = diags.Append(backendConflictMigrateFromStateStoreDiag)
return diags
}
m.StateMigrationInstructions.MigrateFromStateStore = file.MigrateFromStateStore
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Duplicate "migrate_from_state_store" configuration block`,
Detail: fmt.Sprintf(`A "migrate_from_state_store" block was already declared at %s. Only one of these blocks can be include in a module's state migration files.`, m.StateMigrationInstructions.MigrateFromStateStore.DeclRange),
Subject: &file.MigrateFromStateStore.DeclRange,
})
}
}
if file.MigrateFromBackend != nil {
if m.StateMigrationInstructions.MigrateFromBackend == nil {
if m.StateMigrationInstructions.MigrateFromStateStore != nil {
// Parsed a "migrate_from_backend" block after a "migrate_from_state_store" block
diags = diags.Append(backendConflictMigrateFromStateStoreDiag)
return diags
}
if m.StateMigrationInstructions.StateStoreProvider != nil {
// Parsed a "migrate_from_backend" block after a "state_store_provider" block
diags = diags.Append(backendConflictStateStoreProviderDiag)
return diags
}

m.StateMigrationInstructions.MigrateFromBackend = file.MigrateFromBackend
} else {
// Duplicate
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Duplicate "migrate_from_backend" configuration block`,
Detail: fmt.Sprintf(`A "migrate_from_backend" block was already declared at %s. Only one of these blocks can be include in a module's state migration files.`, m.StateMigrationInstructions.MigrateFromBackend.DeclRange),
Subject: &file.MigrateFromBackend.DeclRange,
})
}
}

return diags
}

func (m *Module) mergeFile(file *File) hcl.Diagnostics {
var diags hcl.Diagnostics

Expand Down
11 changes: 11 additions & 0 deletions internal/configs/parser_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
84 changes: 84 additions & 0 deletions internal/configs/parser_config_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,6 +80,76 @@ func (p *Parser) LoadConfigDir(path string, opts ...Option) (*Module, hcl.Diagno
}
}
}
// 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
}

if mod != nil {
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.MigrateFromStateStore
b := mod.StateMigrationInstructions.MigrateFromBackend
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 && 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 "migrate_from_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 migrate_from_state_store block
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Missing "migrate_from_state_store" block for state store migration`,
Detail: `The configuration includes a "state_store_provider" block but is missing the required "migrate_from_state_store" block. Add a "migrate_from_state_store" block to specify the state store to migrate from.`,
})
case ss != nil && ssp != nil:
// Both migrate_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
}
Comment on lines +146 to +149
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This serves a similar purpose as this code in NewModule:

if mod.StateStore != nil {
diags = append(diags, mod.resolveStateStoreProviderType()...)
}

}
}
}

if mod != nil {
mod.SourceDir = path
Expand Down Expand Up @@ -220,6 +291,19 @@ func (p *Parser) loadQueryFiles(basePath string, paths []string) ([]*QueryFile,
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 {
Expand Down
Loading