Skip to content

Commit b952256

Browse files
PSS: Add reusable method for obtaining the provider factory needed for accessing a state store. (#37665)
* Add method to allow accessing factories from locks that are in memory * Create new getStateStoreProviderFactory method for accessing a factory from config * Update getStateStoreProviderFactory to use in memory locks instead of reading them from the deps lock file. Update calling code to accommodate this. * Add tests for getStateStoreProviderFactory, improve errors returned from method * Update test following schema change in simple providers * Move test case into e2etest package, so we protect against environments where building binaries isn't possible * Fix issues with running test in e2etest package * Update code comments Co-authored-by: Radek Simko <[email protected]> --------- Co-authored-by: Radek Simko <[email protected]>
1 parent 1e41449 commit b952256

File tree

11 files changed

+263
-40
lines changed

11 files changed

+263
-40
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package e2etest
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/apparentlymart/go-versions/versions"
13+
tfaddr "github.com/hashicorp/terraform-registry-address"
14+
"github.com/hashicorp/terraform/internal/addrs"
15+
"github.com/hashicorp/terraform/internal/command"
16+
"github.com/hashicorp/terraform/internal/configs"
17+
"github.com/hashicorp/terraform/internal/depsfile"
18+
"github.com/hashicorp/terraform/internal/e2e"
19+
"github.com/hashicorp/terraform/internal/getproviders"
20+
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
21+
)
22+
23+
func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) {
24+
t.Run("gets the matching factory from local provider cache", func(t *testing.T) {
25+
if !canRunGoBuild {
26+
// We're running in a separate-build-then-run context, so we can't
27+
// currently execute this test which depends on being able to build
28+
// new executable at runtime.
29+
//
30+
// (See the comment on canRunGoBuild's declaration for more information.)
31+
t.Skip("can't run without building a new provider executable")
32+
}
33+
34+
// Set up locks
35+
locks := depsfile.NewLocks()
36+
providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/simple")
37+
constraint, err := providerreqs.ParseVersionConstraints(">1.0.0")
38+
if err != nil {
39+
t.Fatalf("test setup failed when making constraint: %s", err)
40+
}
41+
locks.SetProvider(
42+
providerAddr,
43+
versions.MustParseVersion("9.9.9"),
44+
constraint,
45+
[]providerreqs.Hash{""},
46+
)
47+
48+
// Set up a local provider cache for the test to use
49+
// 1. Build a binary for the current platform
50+
simple6Provider := filepath.Join(".", "terraform-provider-simple6")
51+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
52+
// 2. Create a working directory with .terraform/providers directory
53+
td := t.TempDir()
54+
t.Chdir(td)
55+
providerPath := fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/simple/9.9.9/%s", getproviders.CurrentPlatform.String())
56+
err = os.MkdirAll(providerPath, os.ModePerm)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
// 3. Move the binary into the cache folder created above.
61+
os.Rename(simple6ProviderExe, fmt.Sprintf("%s/%s/terraform-provider-simple", td, providerPath))
62+
63+
config := &configs.StateStore{
64+
ProviderAddr: tfaddr.MustParseProviderSource("registry.terraform.io/hashicorp/simple"),
65+
// No other fields necessary for test.
66+
}
67+
68+
// Setup the meta and test GetStateStoreProviderFactory
69+
m := command.Meta{}
70+
factory, diags := m.GetStateStoreProviderFactory(config, locks)
71+
if diags.HasErrors() {
72+
t.Fatalf("unexpected error : %s", err)
73+
}
74+
75+
p, _ := factory()
76+
defer p.Close()
77+
s := p.GetProviderSchema()
78+
expectedProviderDescription := "This is terraform-provider-simple v6"
79+
if s.Provider.Body.Description != expectedProviderDescription {
80+
t.Fatalf("expected description to be %q, but got %q", expectedProviderDescription, s.Provider.Body.Description)
81+
}
82+
})
83+
84+
// See command/meta_backend_test.go for other test cases
85+
}

internal/command/e2etest/providers_schema_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func TestProvidersSchema(t *testing.T) {
7171
"provider": {
7272
"version": 0,
7373
"block": {
74+
"description": "This is terraform-provider-simple v5",
7475
"description_kind": "plain"
7576
}
7677
},
@@ -165,6 +166,7 @@ func TestProvidersSchema(t *testing.T) {
165166
"provider": {
166167
"version": 0,
167168
"block": {
169+
"description": "This is terraform-provider-simple v6",
168170
"description_kind": "plain"
169171
}
170172
},

internal/command/init.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
159159
return back, true, diags
160160
}
161161

162-
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
162+
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
163163
ctx, span := tracer.Start(ctx, "initialize backend")
164164
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
165165
defer span.End()
@@ -187,34 +187,9 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
187187
return nil, true, diags
188188
case root.StateStore != nil:
189189
// state_store config present
190-
// Access provider factories
191-
ctxOpts, err := c.contextOpts()
192-
if err != nil {
193-
diags = diags.Append(err)
194-
return nil, true, diags
195-
}
196-
197-
if root.StateStore.ProviderAddr.IsZero() {
198-
// This should not happen; this data is populated when parsing config,
199-
// even for builtin providers
200-
panic(fmt.Sprintf("unknown provider while beginning to initialize state store %q from provider %q",
201-
root.StateStore.Type,
202-
root.StateStore.Provider.Name))
203-
}
204-
205-
var exists bool
206-
factory, exists := ctxOpts.Providers[root.StateStore.ProviderAddr]
207-
if !exists {
208-
diags = diags.Append(&hcl.Diagnostic{
209-
Severity: hcl.DiagError,
210-
Summary: "Provider unavailable",
211-
Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.",
212-
root.StateStore.Provider.Name,
213-
root.StateStore.ProviderAddr,
214-
root.StateStore.Type,
215-
),
216-
Subject: &root.StateStore.TypeRange,
217-
})
190+
factory, fDiags := c.Meta.GetStateStoreProviderFactory(root.StateStore, configLocks)
191+
diags = diags.Append(fDiags)
192+
if fDiags.HasErrors() {
218193
return nil, true, diags
219194
}
220195

internal/command/init_run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/hashicorp/terraform/internal/command/arguments"
1515
"github.com/hashicorp/terraform/internal/command/views"
1616
"github.com/hashicorp/terraform/internal/configs"
17+
"github.com/hashicorp/terraform/internal/depsfile"
1718
"github.com/hashicorp/terraform/internal/states"
1819
"github.com/hashicorp/terraform/internal/terraform"
1920
"github.com/hashicorp/terraform/internal/tfdiags"
@@ -170,7 +171,10 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
170171
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
171172
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
172173
case initArgs.Backend:
173-
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
174+
// initBackend has new parameters that aren't relevant to the original (unpluggable) version of the init command logic here.
175+
// So for this version of the init command, we pass in empty locks intentionally.
176+
emptyLocks := depsfile.NewLocks()
177+
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, emptyLocks, view)
174178
default:
175179
// load the previously-stored backend config
176180
back, backDiags = c.Meta.backendFromState(ctx)

internal/command/init_run_experiment.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,7 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int
205205
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
206206
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
207207
case initArgs.Backend:
208-
// TODO(SarahFrench/radeksimko) - pass information about config locks (`configLocks`) into initBackend to
209-
// enable PSS
210-
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
208+
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, configLocks, view)
211209
default:
212210
// load the previously-stored backend config
213211
back, backDiags = c.Meta.backendFromState(ctx)

internal/command/meta.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ func (m *Meta) contextOpts() (*terraform.ContextOpts, error) {
546546
opts.Provisioners = m.testingOverrides.Provisioners
547547
} else {
548548
var providerFactories map[addrs.Provider]providers.Factory
549-
providerFactories, err = m.providerFactories()
549+
providerFactories, err = m.ProviderFactories()
550550
opts.Providers = providerFactories
551551
opts.Provisioners = m.provisionerFactories()
552552
}

internal/command/meta_backend.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/hashicorp/terraform/internal/command/views"
3434
"github.com/hashicorp/terraform/internal/command/workdir"
3535
"github.com/hashicorp/terraform/internal/configs"
36+
"github.com/hashicorp/terraform/internal/depsfile"
3637
"github.com/hashicorp/terraform/internal/didyoumean"
3738
"github.com/hashicorp/terraform/internal/plans"
3839
"github.com/hashicorp/terraform/internal/providers"
@@ -1746,6 +1747,57 @@ func (m *Meta) assertSupportedCloudInitOptions(mode cloud.ConfigChangeMode) tfdi
17461747
return diags
17471748
}
17481749

1750+
func (m *Meta) GetStateStoreProviderFactory(config *configs.StateStore, locks *depsfile.Locks) (providers.Factory, tfdiags.Diagnostics) {
1751+
var diags tfdiags.Diagnostics
1752+
1753+
if config == nil || locks == nil {
1754+
panic(fmt.Sprintf("nil config or nil locks passed to GetStateStoreProviderFactory: config %#v, locks %#v", config, locks))
1755+
}
1756+
1757+
if config.ProviderAddr.IsZero() {
1758+
// This should not happen; this data is populated when parsing config,
1759+
// even for builtin providers
1760+
return nil, diags.Append(&hcl.Diagnostic{
1761+
Severity: hcl.DiagError,
1762+
Summary: "Unknown provider used for state storage",
1763+
Detail: "Terraform could not find the provider used with the state_store. This is a bug in Terraform and should be reported.",
1764+
Subject: &config.TypeRange,
1765+
})
1766+
}
1767+
1768+
factories, err := m.ProviderFactoriesFromLocks(locks)
1769+
if err != nil {
1770+
// This may happen if the provider isn't present in the provider cache.
1771+
// This should be caught earlier by logic that diffs the config against the backend state file.
1772+
return nil, diags.Append(&hcl.Diagnostic{
1773+
Severity: hcl.DiagError,
1774+
Summary: "Provider unavailable",
1775+
Detail: fmt.Sprintf("Terraform experienced an error when trying to use provider %s (%q) to initialize the %q state store: %s",
1776+
config.Provider.Name,
1777+
config.ProviderAddr,
1778+
config.Type,
1779+
err),
1780+
Subject: &config.TypeRange,
1781+
})
1782+
}
1783+
1784+
factory, exists := factories[config.ProviderAddr]
1785+
if !exists {
1786+
return nil, diags.Append(&hcl.Diagnostic{
1787+
Severity: hcl.DiagError,
1788+
Summary: "Provider unavailable",
1789+
Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.",
1790+
config.Provider.Name,
1791+
config.ProviderAddr,
1792+
config.Type,
1793+
),
1794+
Subject: &config.TypeRange,
1795+
})
1796+
}
1797+
1798+
return factory, diags
1799+
}
1800+
17491801
//-------------------------------------------------------------------
17501802
// Output constants and initialization code
17511803
//-------------------------------------------------------------------

internal/command/meta_backend_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ import (
1313
"strings"
1414
"testing"
1515

16+
"github.com/apparentlymart/go-versions/versions"
1617
"github.com/hashicorp/cli"
18+
tfaddr "github.com/hashicorp/terraform-registry-address"
1719
"github.com/hashicorp/terraform/internal/addrs"
1820
"github.com/hashicorp/terraform/internal/backend"
1921
"github.com/hashicorp/terraform/internal/cloud"
2022
"github.com/hashicorp/terraform/internal/command/workdir"
2123
"github.com/hashicorp/terraform/internal/configs"
2224
"github.com/hashicorp/terraform/internal/configs/configschema"
2325
"github.com/hashicorp/terraform/internal/copy"
26+
"github.com/hashicorp/terraform/internal/depsfile"
27+
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
2428
"github.com/hashicorp/terraform/internal/plans"
2529
"github.com/hashicorp/terraform/internal/providers"
2630
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
@@ -2397,6 +2401,79 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) {
23972401
}
23982402
}
23992403

2404+
func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) {
2405+
// See internal/command/e2etest/meta_backend_test.go for test case
2406+
// where a provider factory is found using a local provider cache
2407+
2408+
t.Run("returns an error if a matching factory can't be found", func(t *testing.T) {
2409+
// Set up locks
2410+
locks := depsfile.NewLocks()
2411+
providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/simple")
2412+
constraint, err := providerreqs.ParseVersionConstraints(">1.0.0")
2413+
if err != nil {
2414+
t.Fatalf("test setup failed when making constraint: %s", err)
2415+
}
2416+
locks.SetProvider(
2417+
providerAddr,
2418+
versions.MustParseVersion("9.9.9"),
2419+
constraint,
2420+
[]providerreqs.Hash{""},
2421+
)
2422+
2423+
config := &configs.StateStore{
2424+
ProviderAddr: tfaddr.MustParseProviderSource("registry.terraform.io/hashicorp/simple"),
2425+
Provider: &configs.Provider{
2426+
Name: "foobar",
2427+
},
2428+
Type: "store",
2429+
}
2430+
2431+
// Setup the meta and test providerFactoriesDuringInit
2432+
m := testMetaBackend(t, nil)
2433+
_, diags := m.GetStateStoreProviderFactory(config, locks)
2434+
if !diags.HasErrors() {
2435+
t.Fatalf("expected error but got none")
2436+
}
2437+
expectedErr := "Provider unavailable"
2438+
expectedDetail := "Terraform experienced an error when trying to use provider foobar (\"registry.terraform.io/hashicorp/simple\") to initialize the \"store\" state store"
2439+
if diags[0].Description().Summary != expectedErr {
2440+
t.Fatalf("expected error summary to include %q but got: %s",
2441+
expectedErr,
2442+
diags[0].Description().Summary,
2443+
)
2444+
}
2445+
if !strings.Contains(diags[0].Description().Detail, expectedDetail) {
2446+
t.Fatalf("expected error detail to include %q but got: %s",
2447+
expectedErr,
2448+
diags[0].Description().Detail,
2449+
)
2450+
}
2451+
})
2452+
2453+
t.Run("returns an error if provider addr data is missing", func(t *testing.T) {
2454+
// Only minimal locks needed
2455+
locks := depsfile.NewLocks()
2456+
2457+
config := &configs.StateStore{
2458+
ProviderAddr: tfaddr.Provider{}, // Empty
2459+
}
2460+
2461+
// Setup the meta and test providerFactoriesDuringInit
2462+
m := testMetaBackend(t, nil)
2463+
_, diags := m.GetStateStoreProviderFactory(config, locks)
2464+
if !diags.HasErrors() {
2465+
t.Fatal("expected and error but got none")
2466+
}
2467+
expectedErr := "Unknown provider used for state storage"
2468+
if !strings.Contains(diags.Err().Error(), expectedErr) {
2469+
t.Fatalf("expected error to include %q but got: %s",
2470+
expectedErr,
2471+
diags.Err().Error(),
2472+
)
2473+
}
2474+
})
2475+
}
2476+
24002477
func testMetaBackend(t *testing.T, args []string) *Meta {
24012478
var m Meta
24022479
m.Ui = new(cli.MockUi)

0 commit comments

Comments
 (0)