command: Better in-house provider install errors

When init attempts to install a legacy provider required by state and
fails, but another provider with the same type is successfully
installed, this almost definitely means that the user is migrating an
in-house provider. The solution here is to use the `terraform state
replace-provider` subcommand.

This commit makes that next step clearer, by detecting this specific
case, and displaying a list of commands to fix the existing state
provider references.
This commit is contained in:
Alisdair McDiarmid 2020-08-31 17:02:05 -04:00
parent 3547f9e368
commit 9f824c53a5
4 changed files with 147 additions and 5 deletions

View File

@ -427,8 +427,9 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
if moreDiags.HasErrors() { if moreDiags.HasErrors() {
return false, diags return false, diags
} }
stateReqs := make(getproviders.Requirements, 0)
if state != nil { if state != nil {
stateReqs := state.ProviderRequirements() stateReqs = state.ProviderRequirements()
reqs = reqs.Merge(stateReqs) reqs = reqs.Merge(stateReqs)
} }
@ -456,6 +457,11 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
// appear to have been re-namespaced. // appear to have been re-namespaced.
missingProviderErrors := make(map[addrs.Provider]error) missingProviderErrors := make(map[addrs.Provider]error)
// Legacy provider addresses required by source probably refer to in-house
// providers. Capture these for later analysis also, to suggest how to use
// the state replace-provider command to fix this problem.
stateLegacyProviderErrors := make(map[addrs.Provider]error)
// Because we're currently just streaming a series of events sequentially // Because we're currently just streaming a series of events sequentially
// into the terminal, we're showing only a subset of the events to keep // into the terminal, we're showing only a subset of the events to keep
// things relatively concise. Later it'd be nice to have a progress UI // things relatively concise. Later it'd be nice to have a progress UI
@ -509,13 +515,19 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
), ),
)) ))
case getproviders.ErrRegistryProviderNotKnown: case getproviders.ErrRegistryProviderNotKnown:
// Default providers may have no explicit source, and the 404
// error could be caused by re-namespacing. Add the provider
// and error to a map to later check for this case. We don't
// run the check here to keep this event callback simple.
if provider.IsDefault() { if provider.IsDefault() {
// Default providers may have no explicit source, and the 404
// error could be caused by re-namespacing. Add the provider
// and error to a map to later check for this case. We don't
// run the check here to keep this event callback simple.
missingProviderErrors[provider] = err missingProviderErrors[provider] = err
} else if _, ok := stateReqs[provider]; ok && provider.IsLegacy() {
// Legacy provider, from state, not found from any source:
// probably an in-house provider. Record this here to
// faciliate a useful suggestion later.
stateLegacyProviderErrors[provider] = err
} else { } else {
// Otherwise maybe this provider really doesn't exist? Shrug!
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"Failed to query available provider packages", "Failed to query available provider packages",
@ -771,6 +783,53 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
)) ))
} }
// Legacy providers required by state which could not be installed are
// probably in-house providers. If the user has completed the necessary
// steps to make their custom provider available for installation, then
// there should be a provider with the same type selected after the
// installation process completed.
//
// If we detect this specific situation, we can confidently suggest
// that the next step is to run the state replace-provider command to
// update state. We build a map of provider replacements here to ensure
// that we're as concise as possible with the diagnostic.
stateReplaceProviders := make(map[addrs.Provider]addrs.Provider)
for provider, fetchErr := range stateLegacyProviderErrors {
var sameType []addrs.Provider
for p := range selected {
if p.Type == provider.Type {
sameType = append(sameType, p)
}
}
if len(sameType) == 1 {
stateReplaceProviders[provider] = sameType[0]
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install provider",
fmt.Sprintf("Error while installing %s: %s", provider.ForDisplay(), fetchErr),
))
}
}
if len(stateReplaceProviders) > 0 {
var detail strings.Builder
command := "command"
if len(stateReplaceProviders) > 1 {
command = "commands"
}
fmt.Fprintf(&detail, "Found unresolvable legacy provider references in state. It looks like these refer to in-house providers. You can update the resources in state with the following %s:\n", command)
for legacy, replacement := range stateReplaceProviders {
fmt.Fprintf(&detail, "\n terraform state replace-provider %s %s", legacy, replacement)
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install legacy providers required by state",
detail.String(),
))
}
// The errors captured in "err" should be redundant with what we // The errors captured in "err" should be redundant with what we
// received via the InstallerEvents callbacks above, so we'll // received via the InstallerEvents callbacks above, so we'll
// just return those as long as we have some. // just return those as long as we have some.

View File

@ -1026,6 +1026,52 @@ func TestInit_getProviderSource(t *testing.T) {
} }
} }
func TestInit_getProviderLegacyFromState(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-get-provider-legacy-from-state"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
overrides := metaOverridesForProvider(testProvider())
ui := new(cli.MockUi)
providerSource, close := newMockProviderSource(t, map[string][]string{
"acme/alpha": {"1.2.3"},
})
defer close()
m := Meta{
testingOverrides: overrides,
Ui: ui,
ProviderSource: providerSource,
}
c := &InitCommand{
Meta: m,
}
if code := c.Run(nil); code != 1 {
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
}
// Expect this diagnostic output
wants := []string{
"Found unresolvable legacy provider references in state",
"terraform state replace-provider registry.terraform.io/-/alpha registry.terraform.io/acme/alpha",
}
got := ui.ErrorWriter.String()
for _, want := range wants {
if !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got:\n\n%s", want, got)
}
}
// Should still install the alpha provider
exactPath := fmt.Sprintf(".terraform/plugins/registry.terraform.io/acme/alpha/1.2.3/%s", getproviders.CurrentPlatform)
if _, err := os.Stat(exactPath); os.IsNotExist(err) {
t.Fatal("provider 'alpha' not downloaded")
}
}
func TestInit_getProviderInvalidPackage(t *testing.T) { func TestInit_getProviderInvalidPackage(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)

View File

@ -0,0 +1,12 @@
terraform {
required_providers {
alpha = {
source = "acme/alpha"
version = "1.2.3"
}
}
}
resource "alpha_resource" "a" {
index = 1
}

View File

@ -0,0 +1,25 @@
{
"version": 4,
"terraform_version": "0.12.28",
"serial": 1,
"lineage": "481bf512-f245-4c60-42dc-7005f4fa9181",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "alpha_resource",
"name": "a",
"provider": "provider.alpha",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "a",
"index": 1
},
"private": "bnVsbA=="
}
]
}
]
}