Skip to content
Draft
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
54 changes: 54 additions & 0 deletions internal/command/init2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (
"testing"

"github.com/hashicorp/cli"

"github.com/hashicorp/terraform/internal/backend"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
backendCloud "github.com/hashicorp/terraform/internal/cloud"
)

func TestInit2_dynamicSourceErrors(t *testing.T) {
Expand Down Expand Up @@ -718,3 +722,53 @@ func TestPlan_dynamicModuleVersionMismatch(t *testing.T) {
t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want)
}
}

// TestInit_constVarFromCloudBackend verifies that terraform init fetches
// workspace variables from HCP Terraform to resolve const variables used in
// dynamic module source expressions, without requiring -var flags.
func TestInit_constVarFromCloudBackend(t *testing.T) {
// Start a mock HCP Terraform server that serves the "test" workspace and
// returns module_name=example as a Terraform-category variable.
server := cloudTestServerWithVars(t)
defer server.Close()
d := testDisco(server)

// Override the "cloud" backend to use our test server.
previousBackend := backendInit.Backend("cloud")
backendInit.Set("cloud", func() backend.Backend { return backendCloud.New(d) })
defer backendInit.Set("cloud", previousBackend)

// Use the existing fixture: cloud {} block pointing to hashicorp/test,
// variable "module_name" { const = true }, module "example" { source = "./modules/${var.module_name}" }
wd := tempWorkingDirFixture(t, "dynamic-module-sources/get-const-var-backend")
t.Chdir(wd.RootModuleDir())

ui := cli.NewMockUi()
view, done := testView(t)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
WorkingDir: wd,
Services: d,
},
}

// Run init without any -var flags; module_name should be resolved from the backend.
code := c.Run([]string{})
output := done(t)
if code != 0 {
t.Fatalf("expected init to succeed, got exit code %d\n%s", code, output.All())
}

// The module should have been installed into .terraform/modules.
moduleDir := filepath.Join(wd.RootModuleDir(), ".terraform", "modules")
entries, err := os.ReadDir(moduleDir)
if err != nil {
t.Fatalf("expected .terraform/modules to exist after init: %s", err)
}
if len(entries) == 0 {
t.Fatal("expected at least one module entry in .terraform/modules after init")
}
}
14 changes: 14 additions & 0 deletions internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"log"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -168,6 +169,19 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
}

if initArgs.Get {
// If the root module declares any const variables that are not yet
// satisfied by CLI flags, attempt to fetch them from the configured
// backend (e.g. HCP Terraform workspace variables). This is necessary
// so that dynamic module source expressions (e.g. `source = var.path`)
// can be resolved during module installation below.
//
// Non-fatal: if the backend isn't reachable yet (e.g. first-time init
// before credentials are stored) we proceed and let module installation
// fail with a more specific error if needed.
if resolveVarDiags := c.resolveConstVariablesForInit(path, initArgs.ViewType); resolveVarDiags.HasErrors() {
log.Printf("[WARN] init: failed to resolve const variables from backend: %s", resolveVarDiags.Err())
}

modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view)
diags = diags.Append(modsDiags)
if modsAbort || modsDiags.HasErrors() {
Expand Down
49 changes: 49 additions & 0 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2138,6 +2138,55 @@ func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendr
return be, diags
}

// backendForInit is like backend but sets Init: true so it can be used
// during "terraform init" before the backend has been stored in state.
// It is restricted to cloud/backend configs only (not state_store) since
// state_store requires dependency locks that aren't available during init.
//
// This is used by resolveConstVariablesForInit to fetch workspace variables
// early enough for dynamic module source resolution.
func (m *Meta) backendForInit(configPath string, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

if configPath == "" {
configPath = "."
}

root, mDiags := m.loadSingleModule(configPath)
if mDiags.HasErrors() {
diags = diags.Append(mDiags)
return nil, diags
}

var opts *BackendOpts
switch {
case root.Backend != nil:
opts = &BackendOpts{
BackendConfig: root.Backend,
ViewType: viewType,
Init: true,
}
case root.CloudConfig != nil:
backendConfig := root.CloudConfig.ToBackendConfig()
opts = &BackendOpts{
BackendConfig: &backendConfig,
ViewType: viewType,
Init: true,
}
default:
// No cloud/backend config; can't fetch variables.
return nil, nil
}

be, beDiags := m.Backend(opts)
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags
}

return be, diags
}

//-------------------------------------------------------------------
// State Store Config Scenarios
// The functions below cover handling all the various scenarios that
Expand Down
21 changes: 19 additions & 2 deletions internal/command/meta_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ func (m *Meta) normalizePath(path string) string {
// If no const variables are unsatisfied, or if the backend does not support
// supplying variables, this method is a no-op.
func (m *Meta) resolveConstVariables(rootDir string, viewType arguments.ViewType) tfdiags.Diagnostics {
return m.fetchAndApplyConstVariables(rootDir, func() (backendrun.OperationsBackend, tfdiags.Diagnostics) {
return m.backend(rootDir, viewType)
})
}

// resolveConstVariablesForInit is like resolveConstVariables but uses
// backendForInit so it can run during "terraform init" before the backend
// state file exists. This is necessary so that dynamic module source
// expressions (e.g. `source = var.module_path`) can be resolved during
// module installation, which happens before backend init completes.
func (m *Meta) resolveConstVariablesForInit(rootDir string, viewType arguments.ViewType) tfdiags.Diagnostics {
return m.fetchAndApplyConstVariables(rootDir, func() (backendrun.OperationsBackend, tfdiags.Diagnostics) {
return m.backendForInit(rootDir, viewType)
})
}

func (m *Meta) fetchAndApplyConstVariables(rootDir string, loadBackend func() (backendrun.OperationsBackend, tfdiags.Diagnostics)) tfdiags.Diagnostics {
rootMod, diags := m.loadSingleModule(rootDir)
if diags.HasErrors() {
return diags
Expand All @@ -56,9 +73,9 @@ func (m *Meta) resolveConstVariables(rootDir string, viewType arguments.ViewType
return nil
}

b, backendDiags := m.backend(rootDir, viewType)
b, backendDiags := loadBackend()
if backendDiags.HasErrors() {
// Don't report backend init errors here; they'll surface later.
// Don't report backend errors here; they'll surface later during init.
return nil
}

Expand Down
Loading