Commit 1b7f9e47 authored by uturunku1's avatar uturunku1
Browse files

Clarify 'cloud' configuration block change message

When changing a tag after the workspace has been initialized, executing terraform plan or terraform apply would show the error: Terraform Cloud initialization required, please run "terraform init" Reason: Backend configuration changed from "cloud" to terraform cloud. The "Reason" is incorrect. In this case, the "Reason" should read: "Terraform Cloud configuration block changed".
I have adjusted the logic around this switch case statement that was assigning the incorrect value to "Reason" (initReason variable) and also worded things a little different.
parent 21750037
Showing with 468 additions and 327 deletions
+468 -327
......@@ -36,14 +36,14 @@ require (
github.com/hashicorp/go-azure-helpers v0.14.0
github.com/hashicorp/go-checkpoint v0.5.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.5.2
github.com/hashicorp/go-getter v1.5.9
github.com/hashicorp/go-hclog v0.15.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-plugin v1.4.3
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/hashicorp/go-tfe v0.19.1-0.20211020175229-e52963e079d0
github.com/hashicorp/go-tfe v0.20.1-0.20211110172530-c43c6b574caa
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/go-version v1.3.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl/v2 v2.10.1
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2
......@@ -86,7 +86,7 @@ require (
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
golang.org/x/text v0.3.6
golang.org/x/tools v0.1.7
google.golang.org/api v0.44.0-impersonate-preview
......@@ -94,10 +94,10 @@ require (
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
google.golang.org/protobuf v1.27.1
honnef.co/go/tools v0.3.0-0.dev
k8s.io/api v0.0.0-20190620084959-7cf5895f2711
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655
k8s.io/client-go v10.0.0+incompatible
k8s.io/utils v0.0.0-20200411171748-3d5a2fe318e4
k8s.io/api v0.21.3
k8s.io/apimachinery v0.21.3
k8s.io/client-go v0.21.3
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b
)
require (
......@@ -131,13 +131,14 @@ require (
github.com/dylanmei/iso8601 v0.1.0 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/gofrs/uuid v3.3.0+incompatible // indirect
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.0.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d // indirect
github.com/googleapis/gnostic v0.4.1 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
github.com/hashicorp/go-msgpack v0.5.4 // indirect
......@@ -150,11 +151,11 @@ require (
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/json-iterator/go v1.1.7 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/klauspost/compress v1.11.2 // indirect
github.com/kr/pty v1.1.1 // indirect
github.com/kr/pty v1.1.5 // indirect
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
......@@ -167,7 +168,7 @@ require (
github.com/satori/go.uuid v1.2.0 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.1 // indirect
......@@ -176,23 +177,22 @@ require (
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6 // indirect
gopkg.in/inf.v0 v0.9.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
k8s.io/klog v0.4.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.8.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
replace google.golang.org/grpc v1.36.0 => google.golang.org/grpc v1.27.1
replace github.com/golang/mock v1.5.0 => github.com/golang/mock v1.4.4
replace k8s.io/client-go => k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
// github.com/dgrijalva/jwt-go is no longer maintained but is an indirect
// dependency of the old etcdv2 backend, and so we need to keep this working
// until that backend is removed. github.com/golang-jwt/jwt/v3 is a drop-in
......
This diff is collapsed.
......@@ -19,7 +19,7 @@ import (
// prompt creating a different plan than Terraform would by default.
//
// To obtain a full address from a MoveEndpoint you must use
// either the package function UnifyMoveEndpoints (to get an AbsMovable) or
// either the package function UnifyMoveEndpoints (to get an AbsMoveable) or
// the method ConfigMoveable (to get a ConfigMoveable).
type MoveEndpoint struct {
// SourceRange is the location of the physical endpoint address
......@@ -27,15 +27,15 @@ type MoveEndpoint struct {
// configuration expresson.
SourceRange tfdiags.SourceRange
// Internally we (ab)use AbsMovable as the representation of our
// Internally we (ab)use AbsMoveable as the representation of our
// relative address, even though everywhere else in Terraform
// AbsMovable always represents a fully-absolute address.
// AbsMoveable always represents a fully-absolute address.
// In practice, due to the implementation of ParseMoveEndpoint,
// this is always either a ModuleInstance or an AbsResourceInstance,
// and we only consider the possibility of interpreting it as
// a AbsModuleCall or an AbsResource in UnifyMoveEndpoints.
// This is intentionally unexported to encapsulate this unusual
// meaning of AbsMovable.
// meaning of AbsMoveable.
relSubject AbsMoveable
}
......@@ -44,7 +44,7 @@ func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
}
func (e *MoveEndpoint) String() string {
// Our internal pseudo-AbsMovable representing the relative
// Our internal pseudo-AbsMoveable representing the relative
// address (either ModuleInstance or AbsResourceInstance) is
// a good enough proxy for the relative move endpoint address
// serialization.
......
......@@ -131,7 +131,7 @@ func (e *MoveEndpointInModule) Module() Module {
return e.module
}
// InModuleInstance returns an AbsMovable address which concatenates the
// InModuleInstance returns an AbsMoveable address which concatenates the
// given module instance address with the receiver's relative object selection
// to produce one example of an instance that might be affected by this
// move statement.
......
......@@ -12,7 +12,7 @@ import (
func TestParseMoveEndpoint(t *testing.T) {
tests := []struct {
Input string
WantRel AbsMoveable // funny intermediate subset of AbsMovable
WantRel AbsMoveable // funny intermediate subset of AbsMoveable
WantErr string
}{
{
......
......@@ -5,7 +5,7 @@ package addrs
// with any other similar cross-module state refactoring statements we might
// allow.
//
// Note that AbsMovable represents an absolute address relative to the root
// Note that AbsMoveable represents an absolute address relative to the root
// of the configuration, which is different than the direct representation
// of these in configuration where the author gives an address relative to
// the current module where the address is defined. The type MoveEndpoint
......@@ -16,7 +16,7 @@ type AbsMoveable interface {
String() string
}
// The following are all of the possible AbsMovable address types:
// The following are all of the possible AbsMoveable address types:
var (
_ AbsMoveable = AbsResource{}
_ AbsMoveable = AbsResourceInstance{}
......@@ -24,6 +24,19 @@ var (
_ AbsMoveable = AbsModuleCall{}
)
// AbsMoveableResource is an AbsMoveable that is either a resource or a resource
// instance.
type AbsMoveableResource interface {
AbsMoveable
AffectedAbsResource() AbsResource
}
// The following are all of the possible AbsMoveableResource types:
var (
_ AbsMoveableResource = AbsResource{}
_ AbsMoveableResource = AbsResourceInstance{}
)
// ConfigMoveable is similar to AbsMoveable but represents a static object in
// the configuration, rather than an instance of that object created by
// module expansion.
......
......@@ -186,6 +186,11 @@ func (r AbsResource) String() string {
return fmt.Sprintf("%s.%s", r.Module.String(), r.Resource.String())
}
// AffectedAbsResource returns the AbsResource.
func (r AbsResource) AffectedAbsResource() AbsResource {
return r
}
func (r AbsResource) Equal(o AbsResource) bool {
return r.Module.Equal(o.Module) && r.Resource.Equal(o.Resource)
}
......@@ -267,6 +272,14 @@ func (r AbsResourceInstance) String() string {
return fmt.Sprintf("%s.%s", r.Module.String(), r.Resource.String())
}
// AffectedAbsResource returns the AbsResource for the instance.
func (r AbsResourceInstance) AffectedAbsResource() AbsResource {
return AbsResource{
Module: r.Module,
Resource: r.Resource.Resource,
}
}
func (r AbsResourceInstance) Equal(o AbsResourceInstance) bool {
return r.Module.Equal(o.Module) && r.Resource.Equal(o.Resource)
}
......
package kubernetes
import (
"context"
"errors"
"fmt"
"sort"
......@@ -21,6 +22,7 @@ func (b *Backend) Workspaces() ([]string, error) {
}
secrets, err := secretClient.List(
context.Background(),
metav1.ListOptions{
LabelSelector: tfstateKey + "=true",
},
......
package kubernetes
import (
"context"
"fmt"
"math/rand"
"os"
......@@ -124,6 +125,7 @@ func TestBackendLocksSoak(t *testing.T) {
}
func cleanupK8sResources(t *testing.T) {
ctx := context.Background()
// Get a backend to use the k8s client
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"secret_suffix": secretSuffix,
......@@ -138,13 +140,13 @@ func cleanupK8sResources(t *testing.T) {
// Delete secrets
opts := metav1.ListOptions{LabelSelector: tfstateKey + "=true"}
secrets, err := sClient.List(opts)
secrets, err := sClient.List(ctx, opts)
if err != nil {
t.Fatal(err)
}
delProp := metav1.DeletePropagationBackground
delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
delOps := metav1.DeleteOptions{PropagationPolicy: &delProp}
var errs []error
for _, secret := range secrets.Items {
......@@ -155,7 +157,7 @@ func cleanupK8sResources(t *testing.T) {
}
if key == secretSuffix {
err = sClient.Delete(secret.GetName(), delOps)
err = sClient.Delete(ctx, secret.GetName(), delOps)
if err != nil {
errs = append(errs, err)
}
......@@ -168,7 +170,7 @@ func cleanupK8sResources(t *testing.T) {
}
// Delete leases
leases, err := leaseClient.List(opts)
leases, err := leaseClient.List(ctx, opts)
if err != nil {
t.Fatal(err)
}
......@@ -181,7 +183,7 @@ func cleanupK8sResources(t *testing.T) {
}
if key == secretSuffix {
err = leaseClient.Delete(lease.GetName(), delOps)
err = leaseClient.Delete(ctx, lease.GetName(), delOps)
if err != nil {
errs = append(errs, err)
}
......
......@@ -3,6 +3,7 @@ package kubernetes
import (
"bytes"
"compress/gzip"
"context"
"crypto/md5"
"encoding/base64"
"encoding/json"
......@@ -46,7 +47,7 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
if err != nil {
return nil, err
}
secret, err := c.kubernetesSecretClient.Get(secretName, metav1.GetOptions{})
secret, err := c.kubernetesSecretClient.Get(context.Background(), secretName, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
return nil, nil
......@@ -78,6 +79,7 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
}
func (c *RemoteClient) Put(data []byte) error {
ctx := context.Background()
secretName, err := c.createSecretName()
if err != nil {
return err
......@@ -105,14 +107,14 @@ func (c *RemoteClient) Put(data []byte) error {
},
}
secret, err = c.kubernetesSecretClient.Create(secret, metav1.CreateOptions{})
secret, err = c.kubernetesSecretClient.Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
return err
}
}
setState(secret, payload)
_, err = c.kubernetesSecretClient.Update(secret, metav1.UpdateOptions{})
_, err = c.kubernetesSecretClient.Update(ctx, secret, metav1.UpdateOptions{})
return err
}
......@@ -145,6 +147,7 @@ func (c *RemoteClient) Delete() error {
}
func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
ctx := context.Background()
leaseName, err := c.createLeaseName()
if err != nil {
return "", err
......@@ -170,7 +173,7 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
},
}
_, err = c.kubernetesLeaseClient.Create(lease)
_, err = c.kubernetesLeaseClient.Create(ctx, lease, metav1.CreateOptions{})
if err != nil {
return "", err
} else {
......@@ -197,7 +200,7 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
lease.Spec.HolderIdentity = pointer.StringPtr(info.ID)
setLockInfo(lease, info.Marshal())
_, err = c.kubernetesLeaseClient.Update(lease)
_, err = c.kubernetesLeaseClient.Update(ctx, lease, metav1.UpdateOptions{})
if err != nil {
return "", err
}
......@@ -234,7 +237,7 @@ func (c *RemoteClient) Unlock(id string) error {
lease.Spec.HolderIdentity = nil
removeLockInfo(lease)
_, err = c.kubernetesLeaseClient.Update(lease)
_, err = c.kubernetesLeaseClient.Update(context.Background(), lease, metav1.UpdateOptions{})
if err != nil {
lockErr.Err = err
return lockErr
......@@ -276,11 +279,11 @@ func (c *RemoteClient) getLabels() map[string]string {
}
func (c *RemoteClient) getSecret(name string) (*unstructured.Unstructured, error) {
return c.kubernetesSecretClient.Get(name, metav1.GetOptions{})
return c.kubernetesSecretClient.Get(context.Background(), name, metav1.GetOptions{})
}
func (c *RemoteClient) getLease(name string) (*coordinationv1.Lease, error) {
return c.kubernetesLeaseClient.Get(name, metav1.GetOptions{})
return c.kubernetesLeaseClient.Get(context.Background(), name, metav1.GetOptions{})
}
func (c *RemoteClient) deleteSecret(name string) error {
......@@ -296,8 +299,8 @@ func (c *RemoteClient) deleteSecret(name string) error {
}
delProp := metav1.DeletePropagationBackground
delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
return c.kubernetesSecretClient.Delete(name, delOps)
delOps := metav1.DeleteOptions{PropagationPolicy: &delProp}
return c.kubernetesSecretClient.Delete(context.Background(), name, delOps)
}
func (c *RemoteClient) deleteLease(name string) error {
......@@ -313,8 +316,8 @@ func (c *RemoteClient) deleteLease(name string) error {
}
delProp := metav1.DeletePropagationBackground
delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
return c.kubernetesLeaseClient.Delete(name, delOps)
delOps := metav1.DeleteOptions{PropagationPolicy: &delProp}
return c.kubernetesLeaseClient.Delete(context.Background(), name, delOps)
}
func (c *RemoteClient) createSecretName() (string, error) {
......
......@@ -13,6 +13,7 @@ import (
gomock "github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
mocks "github.com/hashicorp/go-tfe/mocks"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
......@@ -576,7 +577,7 @@ func TestCloud_applyAutoApprove(t *testing.T) {
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.NewMockApplies(ctrl)
applyMock := mocks.NewMockApplies(ctrl)
// This needs three new lines because we check for a minimum of three lines
// in the parsing of logs in `opApply` function.
logs := strings.NewReader(applySuccessOneResourceAdded)
......@@ -778,7 +779,7 @@ func TestCloud_applyWithAutoApprove(t *testing.T) {
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.NewMockApplies(ctrl)
applyMock := mocks.NewMockApplies(ctrl)
// This needs three new lines because we check for a minimum of three lines
// in the parsing of logs in `opApply` function.
logs := strings.NewReader(applySuccessOneResourceAdded)
......@@ -1269,7 +1270,7 @@ func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
defer bCleanup()
ctrl := gomock.NewController(t)
policyCheckMock := tfe.NewMockPolicyChecks(ctrl)
policyCheckMock := mocks.NewMockPolicyChecks(ctrl)
// This needs three new lines because we check for a minimum of three lines
// in the parsing of logs in `opApply` function.
logs := strings.NewReader(fmt.Sprintf("%s\n%s", sentinelSoftFail, applySuccessOneResourceAdded))
......@@ -1289,7 +1290,7 @@ func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
policyCheckMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
policyCheckMock.EXPECT().Override(gomock.Any(), gomock.Any()).Return(nil, nil)
b.client.PolicyChecks = policyCheckMock
applyMock := tfe.NewMockApplies(ctrl)
applyMock := mocks.NewMockApplies(ctrl)
// This needs three new lines because we check for a minimum of three lines
// in the parsing of logs in `opApply` function.
logs = strings.NewReader("\n\n\n1 added, 0 changed, 0 destroyed")
......@@ -1348,7 +1349,7 @@ func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) {
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.NewMockApplies(ctrl)
applyMock := mocks.NewMockApplies(ctrl)
// This needs three new lines because we check for a minimum of three lines
// in the parsing of logs in `opApply` function.
logs := strings.NewReader(applySuccessOneResourceAdded)
......
......@@ -477,7 +477,7 @@ func TestCloud_planWithRequiredVariables(t *testing.T) {
defer configCleanup()
defer done(t)
op.Variables = testVariables(terraform.ValueFromCLIArg, "foo") // "bar" variable value missing
op.Variables = testVariables(terraform.ValueFromCLIArg, "foo") // "bar" variable defined in config is missing
op.Workspace = testBackendSingleWorkspaceName
run, err := b.Operation(context.Background(), op)
......@@ -487,7 +487,7 @@ func TestCloud_planWithRequiredVariables(t *testing.T) {
<-run.Done()
// The usual error of a required variable being missing is deferred and the operation
// is successful
// is successful.
if run.Result != backend.OperationSuccess {
t.Fatal("expected plan operation to succeed")
}
......
package cloud
import (
"encoding/json"
"fmt"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
func allowedSourceType(source terraform.ValueSourceType) bool {
......@@ -17,7 +14,7 @@ func allowedSourceType(source terraform.ValueSourceType) bool {
// ParseCloudRunVariables accepts a mapping of unparsed values and a mapping of variable
// declarations and returns a name/value variable map appropriate for an API run context,
// that is, containing declared string variables only sourced from non-file inputs like CLI args
// that is, containing variables only sourced from non-file inputs like CLI args
// and environment variables. However, all variable parsing diagnostics are returned
// in order to allow callers to short circuit cloud runs that contain variable
// declaration or parsing errors. The only exception is that missing required values are not
......@@ -36,16 +33,9 @@ func ParseCloudRunVariables(vv map[string]backend.UnparsedVariableValue, decls m
continue
}
valueData, err := ctyjson.Marshal(v.Value, v.Value.Type())
if err != nil {
return nil, diags.Append(fmt.Errorf("error marshaling input variable value as json: %w", err))
}
var variableValue string
if err = json.Unmarshal(valueData, &variableValue); err != nil {
// This should never happen since cty marshaled the value to begin with without error
return nil, diags.Append(fmt.Errorf("error unmarshaling run variable: %w", err))
}
ret[name] = variableValue
// RunVariables are always expressed as HCL strings
tokens := hclwrite.TokensForValue(v.Value)
ret[name] = string(tokens.Bytes())
}
return ret, diags
......
......@@ -15,11 +15,16 @@ import (
func TestParseCloudRunVariables(t *testing.T) {
t.Run("populates variables from allowed sources", func(t *testing.T) {
vv := map[string]backend.UnparsedVariableValue{
"undeclared": testUnparsedVariableValue{source: terraform.ValueFromCLIArg, value: "0"},
"declaredFromConfig": testUnparsedVariableValue{source: terraform.ValueFromConfig, value: "1"},
"declaredFromNamedFile": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: "2"},
"declaredFromCLIArg": testUnparsedVariableValue{source: terraform.ValueFromCLIArg, value: "3"},
"declaredFromEnvVar": testUnparsedVariableValue{source: terraform.ValueFromEnvVar, value: "4"},
"undeclared": testUnparsedVariableValue{source: terraform.ValueFromCLIArg, value: cty.StringVal("0")},
"declaredFromConfig": testUnparsedVariableValue{source: terraform.ValueFromConfig, value: cty.StringVal("1")},
"declaredFromNamedFileMapString": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar")})},
"declaredFromNamedFileBool": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.BoolVal(true)},
"declaredFromNamedFileNumber": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.NumberIntVal(2)},
"declaredFromNamedFileListString": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.ListVal([]cty.Value{cty.StringVal("2a"), cty.StringVal("2b")})},
"declaredFromNamedFileNull": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.NullVal(cty.String)},
"declaredFromNamedMapComplex": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.MapVal(map[string]cty.Value{"foo": cty.ObjectVal(map[string]cty.Value{"qux": cty.ListVal([]cty.Value{cty.BoolVal(true), cty.BoolVal(false)})})})},
"declaredFromCLIArg": testUnparsedVariableValue{source: terraform.ValueFromCLIArg, value: cty.StringVal("3")},
"declaredFromEnvVar": testUnparsedVariableValue{source: terraform.ValueFromEnvVar, value: cty.StringVal("4")},
}
decls := map[string]*configs.Variable{
......@@ -34,11 +39,66 @@ func TestParseCloudRunVariables(t *testing.T) {
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedFile": {
Name: "declaredFromNamedFile",
"declaredFromNamedFileMapString": {
Name: "declaredFromNamedFileMapString",
Type: cty.Map(cty.String),
ConstraintType: cty.Map(cty.String),
ParsingMode: configs.VariableParseHCL,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedFileBool": {
Name: "declaredFromNamedFileBool",
Type: cty.Bool,
ConstraintType: cty.Bool,
ParsingMode: configs.VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedFileNumber": {
Name: "declaredFromNamedFileNumber",
Type: cty.Number,
ConstraintType: cty.Number,
ParsingMode: configs.VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedFileListString": {
Name: "declaredFromNamedFileListString",
Type: cty.List(cty.String),
ConstraintType: cty.List(cty.String),
ParsingMode: configs.VariableParseHCL,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedFileNull": {
Name: "declaredFromNamedFileNull",
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: configs.VariableParseLiteral,
ParsingMode: configs.VariableParseHCL,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 0},
},
},
"declaredFromNamedMapComplex": {
Name: "declaredFromNamedMapComplex",
Type: cty.DynamicPseudoType,
ConstraintType: cty.DynamicPseudoType,
ParsingMode: configs.VariableParseHCL,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
......@@ -81,10 +141,14 @@ func TestParseCloudRunVariables(t *testing.T) {
},
}
wantVals := make(map[string]string)
wantVals["declaredFromNamedFile"] = "2"
wantVals["declaredFromCLIArg"] = "3"
wantVals["declaredFromEnvVar"] = "4"
wantVals["declaredFromNamedFileBool"] = "true"
wantVals["declaredFromNamedFileNumber"] = "2"
wantVals["declaredFromNamedFileListString"] = `["2a", "2b"]`
wantVals["declaredFromNamedFileNull"] = "null"
wantVals["declaredFromNamedFileMapString"] = "{\n foo = \"bar\"\n}"
wantVals["declaredFromNamedMapComplex"] = "{\n foo = {\n qux = [true, false]\n }\n}"
wantVals["declaredFromCLIArg"] = `"3"`
wantVals["declaredFromEnvVar"] = `"4"`
gotVals, diags := ParseCloudRunVariables(vv, decls)
if diff := cmp.Diff(wantVals, gotVals, cmp.Comparer(cty.Value.RawEquals)); diff != "" {
......@@ -103,12 +167,12 @@ func TestParseCloudRunVariables(t *testing.T) {
type testUnparsedVariableValue struct {
source terraform.ValueSourceType
value string
value cty.Value
}
func (v testUnparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
return &terraform.InputValue{
Value: cty.StringVal(v.value),
Value: v.value,
SourceType: v.source,
SourceRange: tfdiags.SourceRange{
Filename: "fake.tfvars",
......
# How to run tests
To run them, use:
```
TFE_TOKEN=<token> TFE_HOSTNAME=<hostname> TF_ACC=1 go test ./internal/cloud/e2e/... -ldflags "-X \"github.com/hashicorp/terraform/version.Prerelease=<PRE-RELEASE>\""
```
Required flags
* `TF_ACC=1`. This variable is used as part of terraform for tests that make
external network calls. This is needed to run these tests. Without it, the
tests do not run.
* `TFE_TOKEN=<admin token>` and `TFE_HOSTNAME=<hostname>`. The helpers
for these tests require admin access to a TFC/TFE instance.
* `-timeout=30m`. Some of these tests take longer than the default 10m timeout for `go test`.
### Flags
* Use the `-v` flag for normal verbose mode.
* Use the `-tfoutput` flag to print the terraform output to standard out.
* Use `-ldflags` to change the version Prerelease to match a version
available remotely. Some behaviors rely on the exact local version Terraform
being available in TFC/TFE, and manipulating the Prerelease during build is
often the only way to ensure this.
[(More on `-ldflags`.)](https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications)
//go:build e2e
// +build e2e
package main
import (
"context"
"io/ioutil"
"log"
"os"
"testing"
......@@ -17,7 +13,7 @@ import (
)
func Test_terraform_apply_autoApprove(t *testing.T) {
t.Parallel()
skipIfMissingEnvVar(t)
skipWithoutRemoteTerraformVersion(t)
ctx := context.Background()
......@@ -183,12 +179,10 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
},
},
}
for name, tc := range cases {
log.Println("Test: ", name)
for _, tc := range cases {
organization, cleanup := createOrganization(t)
defer cleanup()
exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout))
exp, err := expect.NewConsole(defaultOpts()...)
if err != nil {
t.Fatal(err)
}
......@@ -201,7 +195,6 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
defer os.RemoveAll(tmpDir)
tf := e2e.NewBinary(terraformBin, tmpDir)
tf.AddEnv("TF_LOG=info")
tf.AddEnv(cliConfigFileEnv)
defer tf.Close()
......
//go:build e2e
// +build e2e
package main
import (
"fmt"
"io/ioutil"
"os"
"testing"
......@@ -14,6 +10,7 @@ import (
)
func Test_backend_apply_before_init(t *testing.T) {
skipIfMissingEnvVar(t)
t.Parallel()
skipWithoutRemoteTerraformVersion(t)
......@@ -51,10 +48,8 @@ func Test_backend_apply_before_init(t *testing.T) {
expectedCmdOutput: `Successfully configured the backend "local"!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
command: []string{"apply", "-auto-approve"},
postInputOutput: []string{`Apply complete!`},
},
},
},
......@@ -77,68 +72,70 @@ func Test_backend_apply_before_init(t *testing.T) {
}
for name, tc := range cases {
fmt.Println("Test: ", name)
organization, cleanup := createOrganization(t)
defer cleanup()
exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout))
if err != nil {
t.Fatal(err)
}
defer exp.Close()
tmpDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
organization, cleanup := createOrganization(t)
defer cleanup()
exp, err := expect.NewConsole(defaultOpts()...)
if err != nil {
t.Fatal(err)
}
defer exp.Close()
tf := e2e.NewBinary(terraformBin, tmpDir)
tf.AddEnv("TF_LOG=info")
tf.AddEnv(cliConfigFileEnv)
defer tf.Close()
tmpDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
for _, op := range tc.operations {
op.prep(t, organization.Name, tf.WorkDir())
for _, tfCmd := range op.commands {
cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = exp.Tty()
cmd.Stdout = exp.Tty()
cmd.Stderr = exp.Tty()
tf := e2e.NewBinary(terraformBin, tmpDir)
tf.AddEnv(cliConfigFileEnv)
defer tf.Close()
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
for _, op := range tc.operations {
op.prep(t, organization.Name, tf.WorkDir())
for _, tfCmd := range op.commands {
cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = exp.Tty()
cmd.Stdout = exp.Tty()
cmd.Stderr = exp.Tty()
if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedCmdOutput)
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
}
lenInput := len(tfCmd.userInput)
lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input)
// use the index to find the corresponding
// output that matches the input.
if lenInputOutput-1 >= i {
output := tfCmd.postInputOutput[i]
_, err := exp.ExpectString(output)
if err != nil {
t.Fatal(err)
if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedCmdOutput)
if err != nil {
t.Fatal(err)
}
}
lenInput := len(tfCmd.userInput)
lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input)
// use the index to find the corresponding
// output that matches the input.
if lenInputOutput-1 >= i {
output := tfCmd.postInputOutput[i]
_, err := exp.ExpectString(output)
if err != nil {
t.Fatal(err)
}
}
}
}
}
err = cmd.Wait()
if err != nil && !tfCmd.expectError {
t.Fatal(err)
err = cmd.Wait()
if err != nil && !tfCmd.expectError {
t.Fatal(err)
}
}
}
}
})
}
}
//go:build e2e
// +build e2e
package main
import (
......@@ -10,8 +7,10 @@ import (
"testing"
"time"
expect "github.com/Netflix/go-expect"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/go-uuid"
goversion "github.com/hashicorp/go-version"
tfversion "github.com/hashicorp/terraform/version"
)
......@@ -22,7 +21,6 @@ const (
type tfCommand struct {
command []string
expectedCmdOutput string
expectedErr string
expectError bool
userInput []string
postInputOutput []string
......@@ -38,6 +36,16 @@ type testCases map[string]struct {
validations func(t *testing.T, orgName string)
}
func defaultOpts() []expect.ConsoleOpt {
opts := []expect.ConsoleOpt{
expect.WithDefaultTimeout(expectConsoleTimeout),
}
if verboseMode {
opts = append(opts, expect.WithStdout(os.Stdout))
}
return opts
}
func createOrganization(t *testing.T) (*tfe.Organization, func()) {
ctx := context.Background()
org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
......@@ -93,7 +101,7 @@ func randomString(t *testing.T) string {
}
func terraformConfigLocalBackend() string {
return fmt.Sprintf(`
return `
terraform {
backend "local" {
}
......@@ -102,7 +110,7 @@ terraform {
output "val" {
value = "${terraform.workspace}"
}
`)
`
}
func terraformConfigRemoteBackendName(org, name string) string {
......@@ -193,9 +201,16 @@ func writeMainTF(t *testing.T, block string, dir string) {
f.Close()
}
// Ensure that TFC/E has a particular terraform version.
// The e2e tests rely on the fact that the terraform version in TFC/E is able to
// run the `cloud` configuration block, which is available in 1.1 and will
// continue to be available in later versions. So this function checks that
// there is a version that is >= 1.1.
func skipWithoutRemoteTerraformVersion(t *testing.T) {
version := tfversion.String()
version := tfversion.Version
baseVersion, err := goversion.NewVersion(version)
if err != nil {
t.Fatalf(fmt.Sprintf("Error instantiating go-version for %s", version))
}
opts := tfe.AdminTerraformVersionsListOptions{
ListOptions: tfe.ListOptions{
PageNumber: 1,
......@@ -213,7 +228,12 @@ findTfVersion:
t.Fatalf("Could not retrieve list of terraform versions: %v", err)
}
for _, item := range tfVersionList.Items {
if item.Version == version {
availableVersion, err := goversion.NewVersion(item.Version)
if err != nil {
t.Logf("Error instantiating go-version for %s", item.Version)
continue
}
if availableVersion.Core().GreaterThanOrEqual(baseVersion.Core()) {
hasVersion = true
break findTfVersion
}
......
//go:build e2e
// +build e2e
package main
import (
"fmt"
"io/ioutil"
"os"
"testing"
......@@ -14,6 +10,7 @@ import (
)
func Test_init_with_empty_tags(t *testing.T) {
skipIfMissingEnvVar(t)
t.Parallel()
skipWithoutRemoteTerraformVersion(t)
......@@ -42,68 +39,70 @@ func Test_init_with_empty_tags(t *testing.T) {
}
for name, tc := range cases {
fmt.Println("Test: ", name)
organization, cleanup := createOrganization(t)
defer cleanup()
exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout))
if err != nil {
t.Fatal(err)
}
defer exp.Close()
tmpDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
organization, cleanup := createOrganization(t)
defer cleanup()
exp, err := expect.NewConsole(defaultOpts()...)
if err != nil {
t.Fatal(err)
}
defer exp.Close()
tf := e2e.NewBinary(terraformBin, tmpDir)
tf.AddEnv("TF_LOG=info")
tf.AddEnv(cliConfigFileEnv)
defer tf.Close()
tmpDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
for _, op := range tc.operations {
op.prep(t, organization.Name, tf.WorkDir())
for _, tfCmd := range op.commands {
cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = exp.Tty()
cmd.Stdout = exp.Tty()
cmd.Stderr = exp.Tty()
tf := e2e.NewBinary(terraformBin, tmpDir)
tf.AddEnv(cliConfigFileEnv)
defer tf.Close()
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
for _, op := range tc.operations {
op.prep(t, organization.Name, tf.WorkDir())
for _, tfCmd := range op.commands {
cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = exp.Tty()
cmd.Stdout = exp.Tty()
cmd.Stderr = exp.Tty()
if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedCmdOutput)
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
}
lenInput := len(tfCmd.userInput)
lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input)
// use the index to find the corresponding
// output that matches the input.
if lenInputOutput-1 >= i {
output := tfCmd.postInputOutput[i]
_, err := exp.ExpectString(output)
if err != nil {
t.Fatal(err)
if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedCmdOutput)
if err != nil {
t.Fatal(err)
}
}
lenInput := len(tfCmd.userInput)
lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input)
// use the index to find the corresponding
// output that matches the input.
if lenInputOutput-1 >= i {
output := tfCmd.postInputOutput[i]
_, err := exp.ExpectString(output)
if err != nil {
t.Fatal(err)
}
}
}
}
}
err = cmd.Wait()
if err != nil && !tfCmd.expectError {
t.Fatal(err)
err = cmd.Wait()
if err != nil && !tfCmd.expectError {
t.Fatal(err)
}
}
}
}
})
}
}
//go:build e2e
// +build e2e
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
......@@ -22,13 +20,9 @@ var cliConfigFileEnv string
var tfeClient *tfe.Client
var tfeHostname string
var tfeToken string
var verboseMode bool
func TestMain(m *testing.M) {
log.SetFlags(log.LstdFlags | log.Lshortfile)
if !accTest() {
// if TF_ACC is not set, we want to skip all these tests.
return
}
teardown := setup()
code := m.Run()
teardown()
......@@ -42,7 +36,29 @@ func accTest() bool {
return os.Getenv("TF_ACC") != ""
}
func hasHostname() bool {
return os.Getenv("TFE_HOSTNAME") != ""
}
func hasToken() bool {
return os.Getenv("TFE_TOKEN") != ""
}
func hasRequiredEnvVars() bool {
return accTest() && hasHostname() && hasToken()
}
func skipIfMissingEnvVar(t *testing.T) {
if !hasRequiredEnvVars() {
t.Skip("Skipping test, required environment variables missing. Use `TF_ACC`, `TFE_HOSTNAME`, `TFE_TOKEN`")
}
}
func setup() func() {
tfOutput := flag.Bool("tfoutput", false, "This flag produces the terraform output from tests.")
flag.Parse()
verboseMode = *tfOutput
setTfeClient()
teardown := setupBinary()
......@@ -52,41 +68,38 @@ func setup() func() {
}
func setTfeClient() {
hostname := os.Getenv("TFE_HOSTNAME")
token := os.Getenv("TFE_TOKEN")
if hostname == "" {
log.Fatal("hostname cannot be empty")
}
if token == "" {
log.Fatal("token cannot be empty")
}
tfeHostname = hostname
tfeToken = token
tfeHostname = os.Getenv("TFE_HOSTNAME")
tfeToken = os.Getenv("TFE_TOKEN")
cfg := &tfe.Config{
Address: fmt.Sprintf("https://%s", hostname),
Token: token,
Address: fmt.Sprintf("https://%s", tfeHostname),
Token: tfeToken,
}
// Create a new TFE client.
client, err := tfe.NewClient(cfg)
if err != nil {
log.Fatal(err)
if tfeHostname != "" && tfeToken != "" {
// Create a new TFE client.
client, err := tfe.NewClient(cfg)
if err != nil {
fmt.Printf("Could not create new tfe client: %v\n", err)
os.Exit(1)
}
tfeClient = client
}
tfeClient = client
}
func setupBinary() func() {
log.Println("Setting up terraform binary")
tmpTerraformBinaryDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
log.Fatal(err)
fmt.Printf("Could not create temp directory: %v\n", err)
os.Exit(1)
}
log.Println(tmpTerraformBinaryDir)
currentDir, err := os.Getwd()
defer os.Chdir(currentDir)
if err != nil {
log.Fatal(err)
fmt.Printf("Could not change directories: %v\n", err)
os.Exit(1)
}
// Getting top level dir
dirPaths := strings.Split(currentDir, "/")
......@@ -95,7 +108,8 @@ func setupBinary() func() {
topDir := strings.Join(dirPaths[0:topLevel], "/")
if err := os.Chdir(topDir); err != nil {
log.Fatal(err)
fmt.Printf("Could not change directories: %v\n", err)
os.Exit(1)
}
cmd := exec.Command(
......@@ -106,7 +120,8 @@ func setupBinary() func() {
)
err = cmd.Run()
if err != nil {
log.Fatal(err)
fmt.Printf("Could not run exec command: %v\n", err)
os.Exit(1)
}
credFile := fmt.Sprintf("%s/dev.tfrc", tmpTerraformBinaryDir)
......@@ -124,11 +139,13 @@ func writeCredRC(file string) {
creds := credentialBlock()
f, err := os.Create(file)
if err != nil {
log.Fatal(err)
fmt.Printf("Could not create file: %v\n", err)
os.Exit(1)
}
_, err = f.WriteString(creds)
if err != nil {
log.Fatal(err)
fmt.Printf("Could not write credentials: %v\n", err)
os.Exit(1)
}
f.Close()
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment