From 4ba20f9c1c909a4d89f0b210037e880a8c363581 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 1 Jun 2017 17:57:43 -0700 Subject: [PATCH] command init: show suggested constraints for unconstrained providers When running "terraform init" with providers that are unconstrained, we will now produce information to help the user update configuration to constrain for the particular providers that were chosen, to prevent inadvertently drifting onto a newer major release that might contain breaking changes. A ~> constraint is used here because pinning to a single specific version is expected to create dependency hell when using child modules. By using this constraint mode, which allows minor version upgrades, we avoid the need for users to constantly adjust version constraints across many modules, but make major version upgrades still be opt-in. Any constraint at all in the configuration will prevent the display of these suggestions, so users are free to use stronger or weaker constraints if desired, ignoring the recommendation. --- command/init.go | 46 +++++++++++++++++++++++++++++++++ plugin/discovery/version.go | 8 ++++++ plugin/discovery/version_set.go | 6 +++++ 3 files changed, 60 insertions(+) diff --git a/command/init.go b/command/init.go index ab957ac87..685083a7e 100644 --- a/command/init.go +++ b/command/init.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" getter "github.com/hashicorp/go-getter" @@ -187,6 +188,10 @@ func (c *InitCommand) Run(args []string) int { return 1 } + c.Ui.Output(c.Colorize().Color( + "[reset][bold]Initializing provider plugins...", + )) + err = c.getProviders(path, sMgr.State()) if err != nil { c.Ui.Error(fmt.Sprintf( @@ -249,6 +254,37 @@ func (c *InitCommand) getProviders(path string, state *terraform.State) error { return fmt.Errorf("failed to save provider manifest: %s", err) } + // If any providers have "floating" versions (completely unconstrained) + // we'll suggest the user constrain with a pessimistic constraint to + // avoid implicitly adopting a later major release. + constraintSuggestions := make(map[string]discovery.ConstraintStr) + for name, meta := range chosen { + req := requirements[name] + if req == nil { + // should never happen, but we don't want to crash here, so we'll + // be cautious. + continue + } + + if req.Versions.Unconstrained() { + // meta.Version.MustParse is safe here because our "chosen" metas + // were already filtered for validity of versions. + constraintSuggestions[name] = meta.Version.MustParse().MinorUpgradeConstraintStr() + } + } + if len(constraintSuggestions) != 0 { + names := make([]string, 0, len(constraintSuggestions)) + for name := range constraintSuggestions { + names = append(names, name) + } + sort.Strings(names) + + c.Ui.Output(outputInitProvidersUnconstrained) + for _, name := range names { + c.Ui.Output(fmt.Sprintf("* provider.%s: version = %q", name, constraintSuggestions[name])) + } + } + return nil } @@ -361,3 +397,13 @@ If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your environment. If you forget, other commands will detect it and remind you to do so if necessary. ` + +const outputInitProvidersUnconstrained = ` +The following providers do not have any version constraints in configuration, +so the latest version was installed. + +To prevent automatic upgrades to new major versions that may contain breaking +changes, it is recommended to add version = "..." constraints to the +corresponding provider blocks in configuration, with the constraint strings +suggested below. +` diff --git a/plugin/discovery/version.go b/plugin/discovery/version.go index dab26b321..587ebc6e9 100644 --- a/plugin/discovery/version.go +++ b/plugin/discovery/version.go @@ -1,6 +1,7 @@ package discovery import ( + "fmt" "sort" version "github.com/hashicorp/go-version" @@ -48,6 +49,13 @@ func (v Version) NewerThan(other Version) bool { return v.raw.GreaterThan(other.raw) } +// MinorUpgradeConstraintStr returns a ConstraintStr that would permit +// minor upgrades relative to the receiving version. +func (v Version) MinorUpgradeConstraintStr() ConstraintStr { + segments := v.raw.Segments() + return ConstraintStr(fmt.Sprintf("~> %d.%d", segments[0], segments[1])) +} + type Versions []Version // Sort sorts version from newest to oldest. diff --git a/plugin/discovery/version_set.go b/plugin/discovery/version_set.go index 3ce579689..273aca990 100644 --- a/plugin/discovery/version_set.go +++ b/plugin/discovery/version_set.go @@ -69,3 +69,9 @@ func (s Constraints) Append(other Constraints) Constraints { func (s Constraints) String() string { return s.raw.String() } + +// Unconstrained returns true if and only if the receiver is an empty +// constraint set. +func (s Constraints) Unconstrained() bool { + return len(s.raw) == 0 +}