diff --git a/command/init.go b/command/init.go index d9726ff5f284..6fe243101f10 100644 --- a/command/init.go +++ b/command/init.go @@ -32,7 +32,7 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagFromModule string + var flagFromModule, flagLockfile string var flagBackend, flagGet, flagUpgrade bool var flagPluginPath FlagStringSlice flagConfigExtra := newRawFlags("-backend-config") @@ -47,6 +47,7 @@ func (c *InitCommand) Run(args []string) int { cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") + cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set dependency lockfile mode") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -260,7 +261,7 @@ func (c *InitCommand) Run(args []string) int { } // Now that we have loaded all modules, check the module tree for missing providers. - providersOutput, providersAbort, providerDiags := c.getProviders(config, state, flagUpgrade, flagPluginPath) + providersOutput, providersAbort, providerDiags := c.getProviders(config, state, flagUpgrade, flagPluginPath, flagLockfile) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { c.showDiagnostics(diags) @@ -391,7 +392,7 @@ the backend configuration is present and valid. // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. -func (c *InitCommand) getProviders(config *configs.Config, state *states.State, upgrade bool, pluginDirs []string) (output, abort bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProviders(config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) { // Dev overrides cause the result of "terraform init" to be irrelevant for // any overridden providers, so we'll warn about it to avoid later // confusion when Terraform ends up using a different provider than the @@ -725,6 +726,11 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, mode := providercache.InstallNewProvidersOnly if upgrade { + if flagLockfile == "readonly" { + c.Ui.Error("The -upgrade flag conflicts with -lockfile=readonly.") + return true, true, diags + } + mode = providercache.InstallUpgrades } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) @@ -752,6 +758,12 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, // it's the smallest change relative to what came before it, which was // a hidden JSON file specifically for tracking providers.) if !newLocks.Equal(previousLocks) { + // if readonly mode, suppress changes + if flagLockfile == "readonly" { + log.Println("[DEBUG] init: detected changing dependencies, but suppressed by readonly mode") + return true, false, diags + } + if previousLocks.Empty() { // A change from empty to non-empty is special because it suggests // we're running "terraform init" for the first time against a @@ -960,6 +972,10 @@ Options: -upgrade=false If installing modules (-get) or plugins, ignore previously-downloaded objects and install the latest version allowed within configured constraints. + + -lockfile=MODE Set dependency lockfile mode. + Currently only "readonly" is valid. + ` return strings.TrimSpace(helpText) } diff --git a/command/init_test.go b/command/init_test.go index c0f19b3c3c73..849d5faf9471 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1620,6 +1620,137 @@ provider "registry.terraform.io/hashicorp/test" { } } +func TestInit_providerLockFileReadonly(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("init-provider-lock-file"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.2.3"}, + }) + defer close() + + lockFile := ".terraform.lock.hcl" + + // The hash in here is for the fake package that newMockProviderSource produces + // (so it'll change if newMockProviderSource starts producing different contents) + inputLockFile := strings.TrimSpace(` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" + constraints = "1.2.3" + hashes = [ + "zh:e919b507a91e23a00da5c2c4d0b64bcc7900b68d43b3951ac0f6e5d80387fbdc", + ] +} +`) + + badLockFile := strings.TrimSpace(` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" + constraints = "1.2.3" + hashes = [ + "zh:0000000000000000000000000000000000000000000000000000000000000000", + ] +} +`) + + updatedLockFile := strings.TrimSpace(` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" + constraints = "1.2.3" + hashes = [ + "h1:wlbEC2mChQZ2hhgUhl6SeVLPP7fMqOFUZAQhQ9GIIno=", + "zh:e919b507a91e23a00da5c2c4d0b64bcc7900b68d43b3951ac0f6e5d80387fbdc", + ] +} +`) + + cases := []struct { + desc string + input string + args []string + ok bool + want string + }{ + { + desc: "default", + input: inputLockFile, + args: []string{}, + ok: true, + want: updatedLockFile, + }, + { + desc: "readonly", + input: inputLockFile, + args: []string{"-lockfile=readonly"}, + ok: true, + want: inputLockFile, + }, + { + desc: "conflict", + input: inputLockFile, + args: []string{"-lockfile=readonly", "-upgrade"}, + ok: false, + want: inputLockFile, + }, + { + desc: "checksum mismatch", + input: badLockFile, + args: []string{"-lockfile=readonly"}, + ok: false, + want: badLockFile, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ui := new(cli.MockUi) + m := Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + ProviderSource: providerSource, + } + + c := &InitCommand{ + Meta: m, + } + + // write input lockfile + if err := ioutil.WriteFile(lockFile, []byte(tc.input), 0644); err != nil { + t.Fatalf("failed to write input lockfile: %s", err) + } + + code := c.Run(tc.args) + if tc.ok && code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + if !tc.ok && code == 0 { + t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + } + + buf, err := ioutil.ReadFile(lockFile) + if err != nil { + t.Fatalf("failed to read dependency lock file %s: %s", lockFile, err) + } + buf = bytes.TrimSpace(buf) + if diff := cmp.Diff(tc.want, string(buf)); diff != "" { + t.Errorf("wrong dependency lock file contents\n%s", diff) + } + }) + } +} + func TestInit_pluginDirReset(t *testing.T) { td := testTempDir(t) defer os.RemoveAll(td) diff --git a/website/docs/cli/commands/init.html.md b/website/docs/cli/commands/init.html.md index 21ef20470623..765a60b20dc6 100644 --- a/website/docs/cli/commands/init.html.md +++ b/website/docs/cli/commands/init.html.md @@ -157,6 +157,7 @@ You can modify `terraform init`'s plugin behavior with the following options: You can use `-plugin-dir` as a one-time override for exceptional situations, such as if you are testing a local build of a provider plugin you are currently developing. +- `-lockfile=MODE` Set dependency lockfile mode. Currently only "readonly" is valid. ## Running `terraform init` in automation