mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
* Minor fixes in diagnostics This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Rename test to make it specific to use of backend block in config * Update initBackend to accept whole initArgs collection * Only process --backend-config data, when setting up a `backend`, if that data isn't empty * Simplify how mock provider factories are made in tests * Update mock provider's default logic to track and manage existing workspaces * Add `ProviderSchema` method to `Pluggable` structs. This allows calling code to access the provider schema when using provider configuration data. * Add function for converting a providerreqs.Version to a hashicorp/go-version Version. This is needed for using locks when creating the backend state file. * Implement initial version of init new working directories using `stateStore_C_s`. Default to creating the default workspace if no workspaces exist. * Update test fixtures to match the hashicorp/test mock provider used in PSS tests * Allow tests to obtain locks that include `testingOverrides` providers. The `testingOverrides` field will only be set in tests, so this should not impact end users. * Add tests showing TF can initialize a working directory for the first time (and do the same when forced by -reconfigure flag). Remove replaced tests. * Add -create-default-workspace flag, to be used to disable creating the default workspace by default when -input=false (i.e for use in CI). Refactor creation of default workspace logic. Add tests. * Allow reattached providers to be used during init for PSS * Rename variable to `backendHash` so relation to `backend` is clearer * Allow `(m *Meta) Backend` to return warning diagnostics * Protect against nil testingOverrides in providerFactoriesFromLocks * Add test case seeing what happens if default workspace selected, doesn't exist, but other workspaces do exist. The consequences here are due to using `selectWorkspace` in `stateStore_C_s`, matching what's done in `backend_C_r_s`. * Address code consistency check failure on PR * Refactor use of mock in test that's experiencing EOF error... * Remove test that requires test to supply input for user prompt This test passes when run in isolation but fails when run alongside other tests, even when skipping all other tests using `testStdinPipe`. I don't think the value of this test is great enough to start changing how we test stdin input. * Allow -create-default-workspace to be used regardless of whether input is enabled or disabled * Add TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable * Responses to feedback, including making testStdinPipe helper log details of errors copying data to stdin. Note: We cannot call t.Fatal from a non-test goroutine. * Use Errorf instead * Allow backend state files to not include version data when a builtin or reattached provider is in use. * Add clarifying comment about re-attached providers when finding the matching entry in required_providers * Report that the default workspace was created to the view * Refactor: use error comparison via `errors.Is` to identify when no workspaces exist. * Move handling of TF_ENABLE_PLUGGABLE_STATE_STORAGE into init's ParseInit func. * Validate that PSS-related flags can only be used when experiments are enabled, enforce coupling of PSS-related flags when in use. * Slight rewording of output message about default workspace * Update test to assert new output about default workspace
357 lines
11 KiB
Go
357 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package workdir
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
tfaddr "github.com/hashicorp/terraform-registry-address"
|
|
"github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
func TestParseBackendStateFile(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input string
|
|
Want *BackendStateFile
|
|
WantErr string
|
|
}{
|
|
"empty": {
|
|
Input: ``,
|
|
WantErr: `invalid syntax: unexpected end of JSON input`,
|
|
},
|
|
"empty but valid JSON syntax": {
|
|
Input: `{}`,
|
|
WantErr: `invalid syntax: no format version number`,
|
|
},
|
|
"older version": {
|
|
Input: `{
|
|
"version": 2,
|
|
"terraform_version": "0.3.0"
|
|
}`,
|
|
WantErr: `unsupported backend state version 2; you may need to use Terraform CLI v0.3.0 to work in this directory`,
|
|
},
|
|
"newer version": {
|
|
Input: `{
|
|
"version": 4,
|
|
"terraform_version": "54.23.9"
|
|
}`,
|
|
WantErr: `unsupported backend state version 4; you may need to use Terraform CLI v54.23.9 to work in this directory`,
|
|
},
|
|
"legacy remote state is active": {
|
|
Input: `{
|
|
"version": 3,
|
|
"terraform_version": "0.8.0",
|
|
"remote": {
|
|
"anything": "goes"
|
|
}
|
|
}`,
|
|
WantErr: `this working directory uses legacy remote state and so must first be upgraded using Terraform v0.9`,
|
|
},
|
|
"active backend": {
|
|
Input: `{
|
|
"version": 3,
|
|
"terraform_version": "0.8.0",
|
|
"backend": {
|
|
"type": "treasure_chest_buried_on_a_remote_island",
|
|
"config": {},
|
|
"hash" : 12345
|
|
}
|
|
}`,
|
|
Want: &BackendStateFile{
|
|
Version: 3,
|
|
TFVersion: "0.8.0",
|
|
Backend: &BackendConfigState{
|
|
Type: "treasure_chest_buried_on_a_remote_island",
|
|
ConfigRaw: json.RawMessage("{}"),
|
|
Hash: 12345,
|
|
},
|
|
},
|
|
},
|
|
"active state_store": {
|
|
Input: `{
|
|
"version": 3,
|
|
"terraform_version": "9.9.9",
|
|
"state_store": {
|
|
"type": "foobar_baz",
|
|
"config": {
|
|
"bucket": "my-bucket",
|
|
"region": "saturn"
|
|
},
|
|
"provider": {
|
|
"version": "1.2.3",
|
|
"source": "registry.terraform.io/my-org/foobar",
|
|
"config": {
|
|
"credentials": "./creds.json"
|
|
},
|
|
"hash" : 12345
|
|
},
|
|
"hash" : 12345
|
|
}
|
|
}`,
|
|
Want: &BackendStateFile{
|
|
Version: 3,
|
|
TFVersion: "9.9.9",
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
// Watch out - the number of tabs in the last argument here are load-bearing
|
|
Provider: getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", `{
|
|
"credentials": "./creds.json"
|
|
}`),
|
|
ConfigRaw: json.RawMessage(`{
|
|
"bucket": "my-bucket",
|
|
"region": "saturn"
|
|
}`),
|
|
Hash: 12345,
|
|
},
|
|
},
|
|
},
|
|
"detection of malformed state: conflicting 'backend' and 'state_store' sections": {
|
|
Input: `{
|
|
"version": 3,
|
|
"terraform_version": "9.9.9",
|
|
"backend": {
|
|
"type": "treasure_chest_buried_on_a_remote_island",
|
|
"config": {},
|
|
"hash" : 12345
|
|
},
|
|
"state_store": {
|
|
"type": "foobar_baz",
|
|
"config": {
|
|
"provider": "foobar",
|
|
"bucket": "my-bucket"
|
|
},
|
|
"provider": {
|
|
"version": "1.2.3",
|
|
"source": "registry.terraform.io/my-org/foobar",
|
|
"hash" : 12345
|
|
},
|
|
"hash" : 12345
|
|
}
|
|
}`,
|
|
WantErr: `encountered a malformed backend state file that contains state for both a 'backend' and a 'state_store' block`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, err := ParseBackendStateFile([]byte(test.Input))
|
|
|
|
if test.WantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
|
|
}
|
|
if got, want := err.Error(), test.WantErr; got != want {
|
|
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
if diff := cmp.Diff(test.Want, got); diff != "" {
|
|
t.Errorf("wrong result\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeBackendStateFile(t *testing.T) {
|
|
noVersionData := ""
|
|
|
|
tfVersion := version.Version
|
|
tests := map[string]struct {
|
|
Input *BackendStateFile
|
|
Envs map[string]string
|
|
Want []byte
|
|
WantErr string
|
|
}{
|
|
"encoding a backend state file when state_store is in use": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", `{"foo": "bar"}`),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
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}"),
|
|
},
|
|
"it's valid to record no version data when a builtin provider used for state store": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, noVersionData, string(tfaddr.BuiltInProviderHost), string(tfaddr.BuiltInProviderNamespace), "foobar", `{"foo": "bar"}`),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
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}"),
|
|
},
|
|
"it's valid to record no version data when a re-attached provider used for state store": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, noVersionData, "registry.terraform.io", "hashicorp", "foobar", `{"foo": "bar"}`),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
Envs: map[string]string{
|
|
"TF_REATTACH_PROVIDERS": `{
|
|
"foobar": {
|
|
"Protocol": "grpc",
|
|
"ProtocolVersion": 6,
|
|
"Pid": 12345,
|
|
"Test": true,
|
|
"Addr": {
|
|
"Network": "unix",
|
|
"String":"/var/folders/xx/abcde12345/T/plugin12345"
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
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}"),
|
|
},
|
|
"error when neither backend nor state_store config state are present": {
|
|
Input: &BackendStateFile{},
|
|
Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\"\n}"),
|
|
},
|
|
"error when the provider is neither builtin nor reattached and the provider version is missing": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, noVersionData, "registry.terraform.io", "my-org", "foobar", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
WantErr: `state store is not valid: provider version data is missing`,
|
|
},
|
|
"error when the provider source's hostname is missing": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, "1.2.3", "", "my-org", "foobar", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
|
|
},
|
|
"error when the provider source's hostname and namespace are missing ": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, "1.2.3", "", "", "foobar", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
|
|
},
|
|
"error when the provider source is completely missing ": {
|
|
Input: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, "1.2.3", "", "", "", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
WantErr: `state store is not valid: Empty provider address: Expected address composed of hostname, provider namespace and name`,
|
|
},
|
|
"error when both backend and state_store config state are present": {
|
|
Input: &BackendStateFile{
|
|
Backend: &BackendConfigState{
|
|
Type: "foobar",
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foobar_baz",
|
|
Provider: getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
WantErr: `attempted to encode a malformed backend state file; it contains state for both a 'backend' and a 'state_store' block`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
// Some test cases depend on ENVs, not all
|
|
for k, v := range test.Envs {
|
|
t.Setenv(k, v)
|
|
}
|
|
|
|
got, err := EncodeBackendStateFile(test.Input)
|
|
|
|
if test.WantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), test.WantErr) {
|
|
t.Errorf("wrong error\ngot: %s\nwant: %s", err.Error(), test.WantErr)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
if diff := cmp.Diff(test.Want, got); diff != "" {
|
|
t.Errorf("wrong result\n%s", diff)
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestBackendStateFile_DeepCopy(t *testing.T) {
|
|
|
|
tests := map[string]struct {
|
|
file *BackendStateFile
|
|
}{
|
|
"Deep copy preserves state_store data": {
|
|
file: &BackendStateFile{
|
|
StateStore: &StateStoreConfigState{
|
|
Type: "foo_bar",
|
|
Provider: getTestProviderState(t, "1.2.3", "A", "B", "C", ""),
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
},
|
|
"Deep copy preserves backend data": {
|
|
file: &BackendStateFile{
|
|
Backend: &BackendConfigState{
|
|
Type: "foobar",
|
|
ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
|
|
Hash: 123,
|
|
},
|
|
},
|
|
},
|
|
"Deep copy preserves version and Terraform version data": {
|
|
file: &BackendStateFile{
|
|
Version: 3,
|
|
TFVersion: "9.9.9",
|
|
},
|
|
},
|
|
}
|
|
|
|
for tn, tc := range tests {
|
|
t.Run(tn, func(t *testing.T) {
|
|
copy := tc.file.DeepCopy()
|
|
|
|
if diff := cmp.Diff(copy, tc.file); diff != "" {
|
|
t.Fatalf("unexpected difference in backend state data:\n %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|