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.
This commit is contained in:
Martin Atkins 2017-06-01 17:57:43 -07:00
parent f70318097a
commit 4ba20f9c1c
3 changed files with 60 additions and 0 deletions

View File

@ -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.
`

View File

@ -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.

View File

@ -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
}