2023-05-02 11:33:06 -04:00
// Copyright IBM Corp. 2014, 2026
2023-08-10 18:43:27 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-05-02 11:33:06 -04:00
2021-08-12 15:30:24 -04:00
package cloud
import (
"context"
2022-03-15 17:42:11 -04:00
"reflect"
2021-08-12 15:30:24 -04:00
"testing"
tfe "github.com/hashicorp/go-tfe"
2023-07-19 04:07:46 -04:00
"github.com/zclconf/go-cty/cty"
backendrun: Separate the types/etc for backends that support operations
We previously had all of the types and helpers for all kinds of backends
together in package backend. That kept things relatively simple, but it
also meant that the majority of backends that only deal with remote state
storage ended up still indirectly depending on the entire Terraform modules
runtime, configuration loader, etc, etc, which brings into scope a bunch
of external dependencies that the remote state backends don't really need.
Since backends that support operations are a rare exception, we'll move the
types and helpers for those into a separate package "backendrun", and
then the main package backend can have a much more modest set of types and,
more importantly, a modest set of dependencies on other packages in this
codebase.
This is part of an ongoing effort to reduce the exposure of Terraform Core
and CLI code to the remote backends and vice-versa, so that in the long
run we can more often treat them as separate for dependency maintenance
purposes.
2024-03-11 19:27:44 -04:00
"github.com/hashicorp/terraform/internal/backend/backendrun"
2021-08-12 15:30:24 -04:00
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
2022-03-15 17:42:11 -04:00
"github.com/hashicorp/terraform/internal/terraform"
2026-03-02 11:03:25 -05:00
tftesting "github.com/hashicorp/terraform/internal/terraform/testing"
2022-03-15 17:42:11 -04:00
"github.com/hashicorp/terraform/internal/tfdiags"
2021-08-12 15:30:24 -04:00
)
func TestRemoteStoredVariableValue ( t * testing . T ) {
tests := map [ string ] struct {
Def * tfe . Variable
Want cty . Value
WantError string
} {
"string literal" : {
& tfe . Variable {
Key : "test" ,
Value : "foo" ,
HCL : false ,
Sensitive : false ,
} ,
cty . StringVal ( "foo" ) ,
` ` ,
} ,
"string HCL" : {
& tfe . Variable {
Key : "test" ,
Value : ` "foo" ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . StringVal ( "foo" ) ,
` ` ,
} ,
"list HCL" : {
& tfe . Variable {
Key : "test" ,
Value : ` [] ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . EmptyTupleVal ,
` ` ,
} ,
"null HCL" : {
& tfe . Variable {
Key : "test" ,
Value : ` null ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . NullVal ( cty . DynamicPseudoType ) ,
` ` ,
} ,
"literal sensitive" : {
& tfe . Variable {
Key : "test" ,
HCL : false ,
Sensitive : true ,
} ,
cty . UnknownVal ( cty . String ) ,
` ` ,
} ,
"HCL sensitive" : {
& tfe . Variable {
Key : "test" ,
HCL : true ,
Sensitive : true ,
} ,
cty . DynamicVal ,
` ` ,
} ,
"HCL computation" : {
// This (stored expressions containing computation) is not a case
// we intentionally supported, but it became possible for remote
2024-04-22 15:21:52 -04:00
// operations in Terraform 0.12 (due to HCP Terraform and Terraform Enterprise
2021-08-12 15:30:24 -04:00
// just writing the HCL verbatim into generated `.tfvars` files).
// We support it here for consistency, and we continue to support
// it in both places for backward-compatibility. In practice,
// there's little reason to do computation in a stored variable
// value because references are not supported.
& tfe . Variable {
Key : "test" ,
Value : ` [for v in ["a"] : v] ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . TupleVal ( [ ] cty . Value { cty . StringVal ( "a" ) } ) ,
` ` ,
} ,
"HCL syntax error" : {
& tfe . Variable {
Key : "test" ,
Value : ` [ ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . DynamicVal ,
` Invalid expression for var.test: The value of variable "test" is marked in the remote workspace as being specified in HCL syntax, but the given value is not valid HCL. Stored variable values must be valid literal expressions and may not contain references to other variables or calls to functions. ` ,
} ,
"HCL with references" : {
& tfe . Variable {
Key : "test" ,
Value : ` foo.bar ` ,
HCL : true ,
Sensitive : false ,
} ,
cty . DynamicVal ,
` Invalid expression for var.test: The value of variable "test" is marked in the remote workspace as being specified in HCL syntax, but the given value is not valid HCL. Stored variable values must be valid literal expressions and may not contain references to other variables or calls to functions. ` ,
} ,
}
for name , test := range tests {
t . Run ( name , func ( t * testing . T ) {
v := & remoteStoredVariableValue {
definition : test . Def ,
}
// This ParseVariableValue implementation ignores the parsing mode,
// so we'll just always parse literal here. (The parsing mode is
// selected by the remote server, not by our local configuration.)
gotIV , diags := v . ParseVariableValue ( configs . VariableParseLiteral )
if test . WantError != "" {
if ! diags . HasErrors ( ) {
t . Fatalf ( "missing expected error\ngot: <no error>\nwant: %s" , test . WantError )
}
errStr := diags . Err ( ) . Error ( )
if errStr != test . WantError {
t . Fatalf ( "wrong error\ngot: %s\nwant: %s" , errStr , test . WantError )
}
} else {
if diags . HasErrors ( ) {
t . Fatalf ( "unexpected error\ngot: %s\nwant: <no error>" , diags . Err ( ) . Error ( ) )
}
got := gotIV . Value
if ! test . Want . RawEquals ( got ) {
t . Errorf ( "wrong result\ngot: %#v\nwant: %#v" , got , test . Want )
}
}
} )
}
}
func TestRemoteContextWithVars ( t * testing . T ) {
catTerraform := tfe . CategoryTerraform
catEnv := tfe . CategoryEnv
tests := map [ string ] struct {
Opts * tfe . VariableCreateOptions
WantError string
} {
"Terraform variable" : {
& tfe . VariableCreateOptions {
Category : & catTerraform ,
} ,
` Value for undeclared variable: A variable named "key" was assigned a value, but the root module does not declare a variable of that name. To use this value, add a "variable" block to the configuration. ` ,
} ,
"environment variable" : {
& tfe . VariableCreateOptions {
Category : & catEnv ,
} ,
` ` ,
} ,
}
for name , test := range tests {
t . Run ( name , func ( t * testing . T ) {
configDir := "./testdata/empty"
2021-09-20 17:54:41 -04:00
b , bCleanup := testBackendWithName ( t )
2021-08-12 15:30:24 -04:00
defer bCleanup ( )
2026-03-02 11:03:25 -05:00
_ , configLoader , configCleanup := tftesting . MustLoadConfigForTests ( t , configDir , "tests" )
2021-08-12 15:30:24 -04:00
defer configCleanup ( )
2021-09-20 14:07:53 -04:00
workspaceID , err := b . getRemoteWorkspaceID ( context . Background ( ) , testBackendSingleWorkspaceName )
2021-08-12 15:30:24 -04:00
if err != nil {
t . Fatal ( err )
}
streams , _ := terminal . StreamsForTesting ( t )
view := views . NewStateLocker ( arguments . ViewHuman , views . NewView ( streams ) )
backendrun: Separate the types/etc for backends that support operations
We previously had all of the types and helpers for all kinds of backends
together in package backend. That kept things relatively simple, but it
also meant that the majority of backends that only deal with remote state
storage ended up still indirectly depending on the entire Terraform modules
runtime, configuration loader, etc, etc, which brings into scope a bunch
of external dependencies that the remote state backends don't really need.
Since backends that support operations are a rare exception, we'll move the
types and helpers for those into a separate package "backendrun", and
then the main package backend can have a much more modest set of types and,
more importantly, a modest set of dependencies on other packages in this
codebase.
This is part of an ongoing effort to reduce the exposure of Terraform Core
and CLI code to the remote backends and vice-versa, so that in the long
run we can more often treat them as separate for dependency maintenance
purposes.
2024-03-11 19:27:44 -04:00
op := & backendrun . Operation {
2021-08-12 15:30:24 -04:00
ConfigDir : configDir ,
ConfigLoader : configLoader ,
StateLocker : clistate . NewLocker ( 0 , view ) ,
2021-09-20 14:07:53 -04:00
Workspace : testBackendSingleWorkspaceName ,
2021-08-12 15:30:24 -04:00
}
v := test . Opts
if v . Key == nil {
key := "key"
v . Key = & key
}
b . client . Variables . Create ( context . TODO ( ) , workspaceID , * v )
2021-08-30 18:27:58 -04:00
_ , _ , diags := b . LocalRun ( op )
2021-08-12 15:30:24 -04:00
if test . WantError != "" {
if ! diags . HasErrors ( ) {
t . Fatalf ( "missing expected error\ngot: <no error>\nwant: %s" , test . WantError )
}
errStr := diags . Err ( ) . Error ( )
if errStr != test . WantError {
t . Fatalf ( "wrong error\ngot: %s\nwant: %s" , errStr , test . WantError )
}
// When Context() returns an error, it should unlock the state,
// so re-locking it is expected to succeed.
2021-09-20 14:07:53 -04:00
stateMgr , _ := b . StateMgr ( testBackendSingleWorkspaceName )
2021-08-12 15:30:24 -04:00
if _ , err := stateMgr . Lock ( statemgr . NewLockInfo ( ) ) ; err != nil {
t . Fatalf ( "unexpected error locking state: %s" , err . Error ( ) )
}
} else {
if diags . HasErrors ( ) {
t . Fatalf ( "unexpected error\ngot: %s\nwant: <no error>" , diags . Err ( ) . Error ( ) )
}
// When Context() succeeds, this should fail w/ "workspace already locked"
2021-09-20 14:07:53 -04:00
stateMgr , _ := b . StateMgr ( testBackendSingleWorkspaceName )
2021-08-12 15:30:24 -04:00
if _ , err := stateMgr . Lock ( statemgr . NewLockInfo ( ) ) ; err == nil {
t . Fatal ( "unexpected success locking state after Context" )
}
}
} )
}
}
2022-03-15 17:42:11 -04:00
func TestRemoteVariablesDoNotOverride ( t * testing . T ) {
catTerraform := tfe . CategoryTerraform
varName1 := "key1"
varName2 := "key2"
varName3 := "key3"
varValue1 := "value1"
varValue2 := "value2"
varValue3 := "value3"
tests := map [ string ] struct {
2026-02-17 08:56:46 -05:00
localVariables map [ string ] arguments . UnparsedVariableValue
2022-03-15 17:42:11 -04:00
remoteVariables [ ] * tfe . VariableCreateOptions
expectedVariables terraform . InputValues
} {
"no local variables" : {
2026-02-17 08:56:46 -05:00
map [ string ] arguments . UnparsedVariableValue { } ,
2022-03-15 17:42:11 -04:00
[ ] * tfe . VariableCreateOptions {
{
Key : & varName1 ,
Value : & varValue1 ,
Category : & catTerraform ,
} ,
{
Key : & varName2 ,
Value : & varValue2 ,
Category : & catTerraform ,
} ,
{
Key : & varName3 ,
Value : & varValue3 ,
Category : & catTerraform ,
} ,
} ,
terraform . InputValues {
varName1 : & terraform . InputValue {
Value : cty . StringVal ( varValue1 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName2 : & terraform . InputValue {
Value : cty . StringVal ( varValue2 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName3 : & terraform . InputValue {
Value : cty . StringVal ( varValue3 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
} ,
} ,
"single conflicting local variable" : {
2026-02-17 08:56:46 -05:00
map [ string ] arguments . UnparsedVariableValue {
2022-03-15 17:42:11 -04:00
varName3 : testUnparsedVariableValue { source : terraform . ValueFromNamedFile , value : cty . StringVal ( varValue3 ) } ,
} ,
[ ] * tfe . VariableCreateOptions {
{
Key : & varName1 ,
Value : & varValue1 ,
Category : & catTerraform ,
} , {
Key : & varName2 ,
Value : & varValue2 ,
Category : & catTerraform ,
} , {
Key : & varName3 ,
Value : & varValue3 ,
Category : & catTerraform ,
} ,
} ,
terraform . InputValues {
varName1 : & terraform . InputValue {
Value : cty . StringVal ( varValue1 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName2 : & terraform . InputValue {
Value : cty . StringVal ( varValue2 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName3 : & terraform . InputValue {
Value : cty . StringVal ( varValue3 ) ,
SourceType : terraform . ValueFromNamedFile ,
SourceRange : tfdiags . SourceRange {
Filename : "fake.tfvars" ,
Start : tfdiags . SourcePos { Line : 1 , Column : 1 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 1 , Column : 1 , Byte : 0 } ,
} ,
} ,
} ,
} ,
"no conflicting local variable" : {
2026-02-17 08:56:46 -05:00
map [ string ] arguments . UnparsedVariableValue {
2022-03-15 17:42:11 -04:00
varName3 : testUnparsedVariableValue { source : terraform . ValueFromNamedFile , value : cty . StringVal ( varValue3 ) } ,
} ,
[ ] * tfe . VariableCreateOptions {
{
Key : & varName1 ,
Value : & varValue1 ,
Category : & catTerraform ,
} , {
Key : & varName2 ,
Value : & varValue2 ,
Category : & catTerraform ,
} ,
} ,
terraform . InputValues {
varName1 : & terraform . InputValue {
Value : cty . StringVal ( varValue1 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName2 : & terraform . InputValue {
Value : cty . StringVal ( varValue2 ) ,
SourceType : terraform . ValueFromInput ,
SourceRange : tfdiags . SourceRange {
Filename : "" ,
Start : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 0 , Column : 0 , Byte : 0 } ,
} ,
} ,
varName3 : & terraform . InputValue {
Value : cty . StringVal ( varValue3 ) ,
SourceType : terraform . ValueFromNamedFile ,
SourceRange : tfdiags . SourceRange {
Filename : "fake.tfvars" ,
Start : tfdiags . SourcePos { Line : 1 , Column : 1 , Byte : 0 } ,
End : tfdiags . SourcePos { Line : 1 , Column : 1 , Byte : 0 } ,
} ,
} ,
} ,
} ,
}
for name , test := range tests {
t . Run ( name , func ( t * testing . T ) {
configDir := "./testdata/variables"
b , bCleanup := testBackendWithName ( t )
defer bCleanup ( )
2026-03-02 11:03:25 -05:00
_ , configLoader , configCleanup := tftesting . MustLoadConfigForTests ( t , configDir , "tests" )
2022-03-15 17:42:11 -04:00
defer configCleanup ( )
workspaceID , err := b . getRemoteWorkspaceID ( context . Background ( ) , testBackendSingleWorkspaceName )
if err != nil {
t . Fatal ( err )
}
streams , _ := terminal . StreamsForTesting ( t )
view := views . NewStateLocker ( arguments . ViewHuman , views . NewView ( streams ) )
backendrun: Separate the types/etc for backends that support operations
We previously had all of the types and helpers for all kinds of backends
together in package backend. That kept things relatively simple, but it
also meant that the majority of backends that only deal with remote state
storage ended up still indirectly depending on the entire Terraform modules
runtime, configuration loader, etc, etc, which brings into scope a bunch
of external dependencies that the remote state backends don't really need.
Since backends that support operations are a rare exception, we'll move the
types and helpers for those into a separate package "backendrun", and
then the main package backend can have a much more modest set of types and,
more importantly, a modest set of dependencies on other packages in this
codebase.
This is part of an ongoing effort to reduce the exposure of Terraform Core
and CLI code to the remote backends and vice-versa, so that in the long
run we can more often treat them as separate for dependency maintenance
purposes.
2024-03-11 19:27:44 -04:00
op := & backendrun . Operation {
2022-03-15 17:42:11 -04:00
ConfigDir : configDir ,
ConfigLoader : configLoader ,
StateLocker : clistate . NewLocker ( 0 , view ) ,
Workspace : testBackendSingleWorkspaceName ,
Variables : test . localVariables ,
}
for _ , v := range test . remoteVariables {
b . client . Variables . Create ( context . TODO ( ) , workspaceID , * v )
}
lr , _ , diags := b . LocalRun ( op )
if diags . HasErrors ( ) {
t . Fatalf ( "unexpected error\ngot: %s\nwant: <no error>" , diags . Err ( ) . Error ( ) )
}
// When Context() succeeds, this should fail w/ "workspace already locked"
stateMgr , _ := b . StateMgr ( testBackendSingleWorkspaceName )
if _ , err := stateMgr . Lock ( statemgr . NewLockInfo ( ) ) ; err == nil {
t . Fatal ( "unexpected success locking state after Context" )
}
actual := lr . PlanOpts . SetVariables
expected := test . expectedVariables
for expectedKey := range expected {
actualValue := actual [ expectedKey ]
expectedValue := expected [ expectedKey ]
if ! reflect . DeepEqual ( * actualValue , * expectedValue ) {
t . Fatalf ( "unexpected variable '%s'\ngot: %v\nwant: %v" , expectedKey , actualValue , expectedValue )
}
}
} )
}
}