Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changes/v1.15/ENHANCEMENTS-20251107-140221.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: "init: skip dependencies declared in development override. This allows you to use `terraform init` with developer overrides and install dependencies that are not declared in the override file."
time: 2025-11-07T14:02:21.847382+01:00
custom:
Issue: "27459"
88 changes: 83 additions & 5 deletions internal/command/e2etest/provider_dev_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"github.com/hashicorp/terraform/internal/e2e"
"github.com/hashicorp/terraform/internal/getproviders"
)

// TestProviderDevOverrides is a test for the special dev_overrides setting
Expand Down Expand Up @@ -85,14 +86,91 @@ func TestProviderDevOverrides(t *testing.T) {
t.Errorf("stdout doesn't include the warning about development overrides\nwant: %s\n%s", want, got)
}

stdout, stderr, err = tf.Run("init")
if err == nil {
t.Fatal("expected error: Failed to query available provider packages")
stdout, _, _ = tf.Run("init")
if err != nil {
t.Fatalf("unexpected error: %e", err)
}
if got, want := stdout, `Provider development overrides are in effect`; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include the warning about development overrides\nwant: %s\n%s", want, got)
}
if got, want := stderr, `Failed to query available provider packages`; !strings.Contains(got, want) {
t.Errorf("stderr doesn't include the error about listing unavailable development provider\nwant: %s\n%s", want, got)

if got, want := stdout, "These providers are not installed as part of init since they"; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include init specific warning about consequences of overrides \nwant: %s\n%s", want, got)
}
}

func TestProviderDevOverridesWithProviderToDownload(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
// currently execute this test which depends on being able to build
// new executable at runtime.
//
// (See the comment on canRunGoBuild's declaration for more information.)
t.Skip("can't run without building a new provider executable")
}
t.Parallel()

// This test reaches out to releases.hashicorp.com to download the
// null provider, so it can only run if network access is allowed.
skipIfCannotAccessNetwork(t)

tf := e2e.NewBinary(t, terraformBin, "testdata/provider-dev-override-with-existing")

// In order to do a decent end-to-end test for this case we will need a
// real enough provider plugin to try to run and make sure we are able
// to actually run it. For now we'll use the "test" provider for that,
// because it happens to be in this repository and therefore allows
// us to avoid drawing in anything external, but we might revisit this
// strategy in future if other needs cause us to evolve the test
// provider in a way that makes it less suitable for this particular test,
// such as if it stops being buildable into an independent executable.
providerExeDir := filepath.Join(tf.WorkDir(), "pkgdir")
providerExePrefix := filepath.Join(providerExeDir, "terraform-provider-test_")
providerExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", providerExePrefix)
t.Logf("temporary provider executable is %s", providerExe)

err := ioutil.WriteFile(filepath.Join(tf.WorkDir(), "dev.tfrc"), []byte(fmt.Sprintf(`
provider_installation {
dev_overrides {
"example.com/test/test" = %q
}
direct {}
}
`, providerExeDir)), os.ModePerm)
if err != nil {
t.Fatal(err)
}

tf.AddEnv("TF_CLI_CONFIG_FILE=dev.tfrc")

stdout, stderr, _ := tf.Run("providers")
if err != nil {
t.Fatalf("unexpected error: %s\n%s", err, stderr)
}
if got, want := stdout, `provider[example.com/test/test]`; !strings.Contains(got, want) {
t.Errorf("configuration should depend on %s, but doesn't\n%s", want, got)
}

stdout, _, err = tf.Run("init")
if err != nil {
t.Fatalf("unexpected error: %e", err)
}
if got, want := stdout, `Provider development overrides are in effect`; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include the warning about development overrides\nwant: %s\n%s", want, got)
}
if got, want := stdout, "These providers are not installed as part of init since they were"; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include init specific warning about consequences of overrides \nwant: %s\n got: %s", want, got)
}

// Check if the null provider has been installed
const providerVersion = "3.1.0" // must match the version in the fixture config
pluginDir := filepath.Join(tf.WorkDir(), ".terraform", "providers", "registry.terraform.io", "hashicorp", "null", providerVersion, getproviders.CurrentPlatform.String())
pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerVersion+"_x5")
if getproviders.CurrentPlatform.OS == "windows" {
pluginExe += ".exe" // ugh
}

if _, err := os.Stat(pluginExe); os.IsNotExist(err) {
t.Fatalf("expected plugin executable %s to exist, but it does not", pluginExe)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is where the test will place the temporary build of the test provider.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
terraform {
required_providers {
# this one is overwritten by dev override
simple = {
source = "example.com/test/test"
version = "2.0.0"
}

# this one should still be loaded
null = {
# Our version is intentionally fixed so that we have a fixed
# test case here, though we might have to update this in future
# if e.g. Terraform stops supporting plugin protocol 5, or if
# the null provider is yanked from the registry for some reason.
source = "hashicorp/null"
version = "3.1.0"
}
}
}

data "simple_resource" "test" {
}
3 changes: 2 additions & 1 deletion internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,12 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
if hclDiags.HasErrors() {
return false, true, diags
}

reqs = c.removeDevOverrides(reqs)
if state != nil {
stateReqs := state.ProviderRequirements()
reqs = reqs.Merge(stateReqs)
}

for providerAddr := range reqs {
if providerAddr.IsLegacy() {
diags = diags.Append(tfdiags.Sourceless(
Expand Down
18 changes: 17 additions & 1 deletion internal/command/meta_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
builtinProviders "github.com/hashicorp/terraform/internal/builtin/providers"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/logging"
tfplugin "github.com/hashicorp/terraform/internal/plugin"
tfplugin6 "github.com/hashicorp/terraform/internal/plugin6"
Expand Down Expand Up @@ -176,7 +177,7 @@ func (m *Meta) providerDevOverrideInitWarnings() tfdiags.Diagnostics {
for addr, path := range m.ProviderDevOverrides {
detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path))
}
detailMsg.WriteString("\nSkip terraform init when using provider development overrides. It is not necessary and may error unexpectedly.")
detailMsg.WriteString("\nThese providers are not installed as part of init since they were overwritten. If this is unintentional please re-run without the development overrides set.")
return tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Warning,
Expand All @@ -186,6 +187,21 @@ func (m *Meta) providerDevOverrideInitWarnings() tfdiags.Diagnostics {
}
}

func (m *Meta) removeDevOverrides(reqs providerreqs.Requirements) providerreqs.Requirements {
// Deep copy the requirements to avoid mutating the input
copiedReqs := make(providerreqs.Requirements)
for provider, versions := range reqs {
// Only copy if the provider is not overridden
if _, overridden := m.ProviderDevOverrides[provider]; !overridden {
copiedVersions := make(providerreqs.VersionConstraints, len(versions))
copy(copiedVersions, versions)
copiedReqs[provider] = copiedVersions
}
}

return copiedReqs
}

// providerDevOverrideRuntimeWarnings returns a diagnostics that contains at
// least one warning if and only if there is at least one provider development
// override in effect. If not, the result is always empty. The result never
Expand Down