Skip to content

Commit

Permalink
command/init: Add a new flag -lockfile=readonly
Browse files Browse the repository at this point in the history
Fixes hashicorp#27506

Add a new flag `-lockfile=readonly` to `terraform init`.
It would be useful to allow us to suppress dependency lockfile changes
explicitly.

The type of the `-lockfile` flag is string rather than bool, leaving
room for future extensions to other behavior variants.

The readonly mode suppresses lockfile changes, but should verify
checksums against the information already recorded. It should conflict
with the `-upgrade` flag.

Note: In the original use-case described in hashicorp#27506, I would like to
suppress adding zh hashes, but a test code here suppresses adding h1
hashes because it's easy for testing.
  • Loading branch information
minamijoyo committed Mar 8, 2021
1 parent 98899df commit 8ecb5ff
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 3 deletions.
22 changes: 19 additions & 3 deletions command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
131 changes: 131 additions & 0 deletions command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions website/docs/cli/commands/init.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 8ecb5ff

Please sign in to comment.