Skip to content

Commit 4a28cf9

Browse files
committed
Allow backend state files to not include version data when a builtin or reattached provider is in use.
1 parent ce8c27c commit 4a28cf9

File tree

5 files changed

+102
-62
lines changed

5 files changed

+102
-62
lines changed

internal/command/meta_backend.go

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/hashicorp/terraform/internal/depsfile"
4141
"github.com/hashicorp/terraform/internal/didyoumean"
4242
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
43+
"github.com/hashicorp/terraform/internal/getproviders/reattach"
4344
"github.com/hashicorp/terraform/internal/plans"
4445
"github.com/hashicorp/terraform/internal/providers"
4546
"github.com/hashicorp/terraform/internal/states"
@@ -1667,42 +1668,26 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide
16671668
s = workdir.NewBackendStateFile()
16681669
}
16691670

1670-
var pVersion *version.Version
1671-
if c.ProviderAddr.Equals(addrs.NewBuiltInProvider("terraform")) {
1672-
// If we're handling the builtin "terraform" provider then there's no version information to store in the dependency lock file, so don't access it.
1673-
// We must record a value into the backend state file, and we cannot include a value that changes (e.g. the Terraform core binary version) as migration
1674-
// is impossible with builtin providers.
1675-
// So, we use an arbitrary stand-in version.
1676-
standInVersion, err := version.NewVersion("0.0.1")
1677-
if err != nil {
1678-
diags = diags.Append(fmt.Errorf("Error when creating a backend state file. This is a bug in Terraform and should be reported: %w",
1679-
err))
1680-
return nil, diags
1681-
}
1682-
pVersion = standInVersion
1671+
var pVersion *version.Version // This will remain nil for builtin providers or unmanaged providers.
1672+
if c.ProviderAddr.Hostname == addrs.BuiltInProviderHost {
1673+
diags = diags.Append(&hcl.Diagnostic{
1674+
Severity: hcl.DiagWarning,
1675+
Summary: "State storage is using a builtin provider",
1676+
Detail: "Terraform is using a builtin provider for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.",
1677+
})
16831678
} else {
1684-
isReattached, err := isProviderReattached(c.ProviderAddr)
1679+
isReattached, err := reattach.IsProviderReattached(c.ProviderAddr, os.Getenv("TF_REATTACH_PROVIDERS"))
16851680
if err != nil {
16861681
diags = diags.Append(fmt.Errorf("Error determining if the state storage provider is reattached or not. This is a bug in Terraform and should be reported: %w",
16871682
err))
16881683
return nil, diags
16891684
}
16901685
if isReattached {
1691-
// If the provider is unmanaged then it won't be in the locks.
1692-
// If there are no locks then there's no version information to for us to access and use when creating the backend state file.
1693-
// So, we use an arbitrary stand-in version.
16941686
diags = diags.Append(&hcl.Diagnostic{
16951687
Severity: hcl.DiagWarning,
16961688
Summary: "State storage provider is not managed by Terraform",
1697-
Detail: "Terraform is using a provider supplied via TF_REATTACH_PROVIDERS for initializing state storage. This will affect Terraform's ability to detect when state migrations are required.",
1689+
Detail: "Terraform is using a provider supplied via TF_REATTACH_PROVIDERS for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.",
16981690
})
1699-
standInVersion, err := version.NewVersion("0.0.1")
1700-
if err != nil {
1701-
diags = diags.Append(fmt.Errorf("Error when creating a backend state file. This is a bug in Terraform and should be reported: %w",
1702-
err))
1703-
return nil, diags
1704-
}
1705-
pVersion = standInVersion
17061691
} else {
17071692
// The provider is not built in and is being managed by Terraform
17081693
// This is the most common scenario, by far.
@@ -1804,29 +1789,6 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide
18041789
return b, diags
18051790
}
18061791

1807-
// isProviderReattached determines if a given provider is being supplied to Terraform via the TF_REATTACH_PROVIDERS
1808-
// environment variable.
1809-
func isProviderReattached(provider addrs.Provider) (bool, error) {
1810-
in := os.Getenv("TF_REATTACH_PROVIDERS")
1811-
if in != "" {
1812-
var m map[string]any
1813-
err := json.Unmarshal([]byte(in), &m)
1814-
if err != nil {
1815-
return false, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err)
1816-
}
1817-
for p := range m {
1818-
a, diags := addrs.ParseProviderSourceString(p)
1819-
if diags.HasErrors() {
1820-
return false, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err())
1821-
}
1822-
if a.Equals(provider) {
1823-
return true, nil
1824-
}
1825-
}
1826-
}
1827-
return false, nil
1828-
}
1829-
18301792
// createDefaultWorkspace receives a backend made using a pluggable state store, and details about that store's config,
18311793
// and persists an empty state file in the default workspace. By creating this artifact we ensure that the default
18321794
// workspace is created and usable by Terraform in later operations.

internal/command/workdir/backend_state_test.go

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/google/go-cmp/cmp"
12+
tfaddr "github.com/hashicorp/terraform-registry-address"
1213
"github.com/hashicorp/terraform/version"
1314
)
1415

@@ -160,9 +161,12 @@ func TestParseBackendStateFile(t *testing.T) {
160161
}
161162

162163
func TestEncodeBackendStateFile(t *testing.T) {
164+
noVersionData := ""
165+
163166
tfVersion := version.Version
164167
tests := map[string]struct {
165168
Input *BackendStateFile
169+
Envs map[string]string
166170
Want []byte
167171
WantErr string
168172
}{
@@ -177,11 +181,58 @@ func TestEncodeBackendStateFile(t *testing.T) {
177181
},
178182
Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": \"1.2.3\",\n \"source\": \"registry.terraform.io/my-org/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"),
179183
},
180-
"it returns an error when neither backend nor state_store config state are present": {
184+
"it's valid to record no version data when a builtin provider used for state store": {
185+
Input: &BackendStateFile{
186+
StateStore: &StateStoreConfigState{
187+
Type: "foobar_baz",
188+
Provider: getTestProviderState(t, noVersionData, string(tfaddr.BuiltInProviderHost), string(tfaddr.BuiltInProviderNamespace), "foobar", `{"foo": "bar"}`),
189+
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
190+
Hash: 123,
191+
},
192+
},
193+
Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"terraform.io/builtin/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"),
194+
},
195+
"it's valid to record no version data when a re-attached provider used for state store": {
196+
Input: &BackendStateFile{
197+
StateStore: &StateStoreConfigState{
198+
Type: "foobar_baz",
199+
Provider: getTestProviderState(t, noVersionData, "registry.terraform.io", "hashicorp", "foobar", `{"foo": "bar"}`),
200+
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
201+
Hash: 123,
202+
},
203+
},
204+
Envs: map[string]string{
205+
"TF_REATTACH_PROVIDERS": `{
206+
"foobar": {
207+
"Protocol": "grpc",
208+
"ProtocolVersion": 6,
209+
"Pid": 12345,
210+
"Test": true,
211+
"Addr": {
212+
"Network": "unix",
213+
"String":"/var/folders/xx/abcde12345/T/plugin12345"
214+
}
215+
}
216+
}`,
217+
},
218+
Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"registry.terraform.io/hashicorp/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"),
219+
},
220+
"error when neither backend nor state_store config state are present": {
181221
Input: &BackendStateFile{},
182222
Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\"\n}"),
183223
},
184-
"it returns an error when the provider source's hostname is missing": {
224+
"error when the provider is neither builtin nor reattached and the provider version is missing": {
225+
Input: &BackendStateFile{
226+
StateStore: &StateStoreConfigState{
227+
Type: "foobar_baz",
228+
Provider: getTestProviderState(t, noVersionData, "registry.terraform.io", "my-org", "foobar", ""),
229+
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
230+
Hash: 123,
231+
},
232+
},
233+
WantErr: `state store is not valid: provider version data is missing`,
234+
},
235+
"error when the provider source's hostname is missing": {
185236
Input: &BackendStateFile{
186237
StateStore: &StateStoreConfigState{
187238
Type: "foobar_baz",
@@ -192,7 +243,7 @@ func TestEncodeBackendStateFile(t *testing.T) {
192243
},
193244
WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
194245
},
195-
"it returns an error when the provider source's hostname and namespace are missing ": {
246+
"error when the provider source's hostname and namespace are missing ": {
196247
Input: &BackendStateFile{
197248
StateStore: &StateStoreConfigState{
198249
Type: "foobar_baz",
@@ -203,7 +254,7 @@ func TestEncodeBackendStateFile(t *testing.T) {
203254
},
204255
WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
205256
},
206-
"it returns an error when the provider source is completely missing ": {
257+
"error when the provider source is completely missing ": {
207258
Input: &BackendStateFile{
208259
StateStore: &StateStoreConfigState{
209260
Type: "foobar_baz",
@@ -214,7 +265,7 @@ func TestEncodeBackendStateFile(t *testing.T) {
214265
},
215266
WantErr: `state store is not valid: Empty provider address: Expected address composed of hostname, provider namespace and name`,
216267
},
217-
"it returns an error when both backend and state_store config state are present": {
268+
"error when both backend and state_store config state are present": {
218269
Input: &BackendStateFile{
219270
Backend: &BackendConfigState{
220271
Type: "foobar",
@@ -234,6 +285,11 @@ func TestEncodeBackendStateFile(t *testing.T) {
234285

235286
for name, test := range tests {
236287
t.Run(name, func(t *testing.T) {
288+
// Some test cases depend on ENVs, not all
289+
for k, v := range test.Envs {
290+
t.Setenv(k, v)
291+
}
292+
237293
got, err := EncodeBackendStateFile(test.Input)
238294

239295
if test.WantErr != "" {

internal/command/workdir/statestore_config_state.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"os"
1011

1112
version "github.com/hashicorp/go-version"
1213
tfaddr "github.com/hashicorp/terraform-registry-address"
1314
"github.com/hashicorp/terraform/internal/configs/configschema"
15+
"github.com/hashicorp/terraform/internal/getproviders/reattach"
1416
"github.com/hashicorp/terraform/internal/plans"
1517
"github.com/zclconf/go-cty/cty"
1618
ctyjson "github.com/zclconf/go-cty/cty/json"
@@ -40,13 +42,13 @@ func (s *StateStoreConfigState) Validate() error {
4042

4143
// Are any bits of data totally missing?
4244
if s.Empty() {
43-
return fmt.Errorf("state store is not valid: data is empty")
45+
return fmt.Errorf("attempted to encode a malformed backend state file; data is empty")
4446
}
45-
if s.Provider == nil {
46-
return fmt.Errorf("state store is not valid: provider data is missing")
47+
if s.Type == "" {
48+
return fmt.Errorf("attempted to encode a malformed backend state file; state store type is missing")
4749
}
48-
if s.Provider.Version == nil {
49-
return fmt.Errorf("state store is not valid: version data is missing")
50+
if s.Provider == nil {
51+
return fmt.Errorf("attempted to encode a malformed backend state file; provider data is missing")
5052
}
5153
if s.ConfigRaw == nil {
5254
return fmt.Errorf("attempted to encode a malformed backend state file; state_store configuration data is missing")
@@ -58,6 +60,18 @@ func (s *StateStoreConfigState) Validate() error {
5860
return fmt.Errorf("state store is not valid: %w", err)
5961
}
6062

63+
// Version information is required if the provider isn't builtin or unmanaged by Terraform
64+
isReattached, err := reattach.IsProviderReattached(*s.Provider.Source, os.Getenv("TF_REATTACH_PROVIDERS"))
65+
if err != nil {
66+
return fmt.Errorf("error determining if state storage provider is reattached: %w", err)
67+
}
68+
if (s.Provider.Source.Hostname != tfaddr.BuiltInProviderHost) &&
69+
!isReattached {
70+
if s.Provider.Version == nil {
71+
return fmt.Errorf("state store is not valid: provider version data is missing")
72+
}
73+
}
74+
6175
return nil
6276
}
6377

internal/command/workdir/testing.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ import (
1717
func getTestProviderState(t *testing.T, semVer, hostname, namespace, typeName, config string) *ProviderConfigState {
1818
t.Helper()
1919

20-
ver, err := version.NewSemver(semVer)
21-
if err != nil {
22-
t.Fatalf("test setup failed when creating version.Version: %s", err)
20+
var ver *version.Version
21+
if semVer == "" {
22+
// Allow passing no version in; leave ver nil
23+
ver = nil
24+
} else {
25+
var err error
26+
ver, err = version.NewSemver(semVer)
27+
if err != nil {
28+
t.Fatalf("test setup failed when creating version.Version: %s", err)
29+
}
2330
}
2431

2532
return &ProviderConfigState{

internal/getproviders/reattach/reattach.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net"
1010

1111
"github.com/hashicorp/go-plugin"
12+
tfaddr "github.com/hashicorp/terraform-registry-address"
1213
"github.com/hashicorp/terraform/internal/addrs"
1314
)
1415

@@ -88,7 +89,7 @@ func ParseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfi
8889
// environment variable.
8990
//
9091
// Calling code is expected to pass in a provider address and the value of os.Getenv("TF_REATTACH_PROVIDERS")
91-
func IsProviderReattached(provider addrs.Provider, in string) (bool, error) {
92+
func IsProviderReattached(provider tfaddr.Provider, in string) (bool, error) {
9293
providers, err := ParseReattachProviders(in)
9394
if err != nil {
9495
return false, err

0 commit comments

Comments
 (0)