Add auto-approve logic, e2e tests

This commit is contained in:
Omar Ismail 2021-09-22 17:53:33 -04:00 committed by Chris Arcand
parent ee384e8716
commit a387af6c61
9 changed files with 646 additions and 68 deletions

4
go.mod
View File

@ -22,7 +22,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
github.com/go-test/deep v1.0.3
github.com/golang/mock v1.5.0
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.5
github.com/google/uuid v1.2.0
@ -40,7 +40,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-plugin v1.4.3
github.com/hashicorp/go-retryablehttp v0.5.2
github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf
github.com/hashicorp/go-tfe v0.19.1-0.20210922134841-a2c1784e9c00
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f

8
go.sum
View File

@ -241,8 +241,9 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -375,8 +376,8 @@ github.com/hashicorp/go-slug v0.7.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf h1:Tn5cI9kacNyO40ztxmwfAaHrOGd7dELLSAueV2Xfv38=
github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf/go.mod h1:7lChm1Mjsh0ofrUNkP8MHljUFrnKNZNTw36S6qSbJZU=
github.com/hashicorp/go-tfe v0.19.1-0.20210922134841-a2c1784e9c00 h1:51ARk47jO4piKzhhbwk6u67ErvSuBj4cu2f2VS9HkgI=
github.com/hashicorp/go-tfe v0.19.1-0.20210922134841-a2c1784e9c00/go.mod h1:U5Iy307L+MazGg0uF8annDtaxAbPp4ElFZ9uPMrjw/I=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@ -933,6 +934,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=

View File

@ -181,72 +181,40 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
}
// Return if the run cannot be confirmed.
if !w.AutoApply && !r.Actions.IsConfirmable {
if !op.AutoApprove && !r.Actions.IsConfirmable {
return r, nil
}
// Since we already checked the permissions before creating the run
// this should never happen. But it doesn't hurt to keep this in as
// a safeguard for any unexpected situations.
if !w.AutoApply && !r.Permissions.CanApply {
// Make sure we discard the run if possible.
if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil {
switch op.PlanMode {
case plans.DestroyMode:
return r, generalError("Failed to discard destroy", err)
default:
return r, generalError("Failed to discard apply", err)
}
}
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Insufficient rights to approve the pending changes",
fmt.Sprintf("There are pending changes, but the provided credentials have "+
"insufficient rights to approve them. The run will be discarded to prevent "+
"it from blocking the queue waiting for external approval. To queue a run "+
"that can be approved by someone else, please use the 'Queue Plan' button in "+
"the web UI:\nhttps://%s/app/%s/%s/runs", b.hostname, b.organization, op.Workspace),
))
return r, diags.Err()
}
mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove
if !w.AutoApply {
if mustConfirm {
opts := &terraform.InputOpts{Id: "approve"}
if mustConfirm {
opts := &terraform.InputOpts{Id: "approve"}
if op.PlanMode == plans.DestroyMode {
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
} else {
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will perform the actions described above.\n" +
"Only 'yes' will be accepted to approve."
}
err = b.confirm(stopCtx, op, opts, r, "yes")
if err != nil && err != errRunApproved {
return r, err
}
if op.PlanMode == plans.DestroyMode {
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
} else {
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will perform the actions described above.\n" +
"Only 'yes' will be accepted to approve."
}
if err != errRunApproved {
if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
return r, generalError("Failed to approve the apply command", err)
}
err = b.confirm(stopCtx, op, opts, r, "yes")
if err != nil && err != errRunApproved {
return r, err
}
} else {
// If we don't need to ask for confirmation, insert a blank
// line to separate the ouputs.
if b.CLI != nil {
b.CLI.Output("")
}
}
// If we don't need to ask for confirmation, insert a blank
// line to separate the ouputs.
if w.AutoApply || !mustConfirm {
if b.CLI != nil {
b.CLI.Output("")
if !op.AutoApprove && err != errRunApproved {
if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
return r, generalError("Failed to approve the apply command", err)
}
}

View File

@ -2,6 +2,7 @@ package cloud
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
@ -9,6 +10,7 @@ import (
"testing"
"time"
gomock "github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
@ -697,6 +699,14 @@ func TestCloud_applyNoApprove(t *testing.T) {
func TestCloud_applyAutoApprove(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.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)
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
b.client.Applies = applyMock
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup()
@ -888,17 +898,24 @@ func TestCloud_applyDiscardedExternally(t *testing.T) {
}
}
func TestCloud_applyWithAutoApply(t *testing.T) {
func TestCloud_applyWithAutoApprove(t *testing.T) {
b, bCleanup := testBackendWithPrefix(t)
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.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)
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
b.client.Applies = applyMock
// Create a named workspace that auto applies.
_, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
AutoApply: tfe.Bool(true),
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
},
)
if err != nil {
@ -916,6 +933,7 @@ func TestCloud_applyWithAutoApply(t *testing.T) {
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = "prod"
op.AutoApprove = true
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -1374,6 +1392,34 @@ func TestCloud_applyPolicySoftFail(t *testing.T) {
func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
ctrl := gomock.NewController(t)
policyCheckMock := tfe.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))
pc := &tfe.PolicyCheck{
ID: "pc-1",
Actions: &tfe.PolicyActions{
IsOverridable: true,
},
Permissions: &tfe.PolicyPermissions{
CanOverride: true,
},
Scope: tfe.PolicyScopeOrganization,
Status: tfe.PolicySoftFailed,
}
policyCheckMock.EXPECT().Read(gomock.Any(), gomock.Any()).Return(pc, nil)
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)
// 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")
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
b.client.Applies = applyMock
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
defer configCleanup()
@ -1422,17 +1468,24 @@ func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
}
}
func TestCloud_applyPolicySoftFailAutoApply(t *testing.T) {
func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
ctrl := gomock.NewController(t)
applyMock := tfe.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)
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
b.client.Applies = applyMock
// Create a named workspace that auto applies.
_, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
AutoApply: tfe.Bool(true),
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
},
)
if err != nil {
@ -1451,6 +1504,7 @@ func TestCloud_applyPolicySoftFailAutoApply(t *testing.T) {
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = "prod"
op.AutoApprove = true
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -1465,7 +1519,7 @@ func TestCloud_applyPolicySoftFailAutoApply(t *testing.T) {
t.Fatalf("expected a non-empty plan")
}
if len(input.answers) != 1 {
if len(input.answers) != 2 {
t.Fatalf("expected an unused answer, got: %v", input.answers)
}
@ -1656,3 +1710,28 @@ func TestCloud_applyVersionCheck(t *testing.T) {
})
}
}
const applySuccessOneResourceAdded = `
Terraform v0.11.10
Initializing plugins and modules...
null_resource.hello: Creating...
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
`
const sentinelSoftFail = `
Sentinel Result: false
Sentinel evaluated to false because one or more Sentinel policies evaluated
to false. This false was not due to an undefined value or runtime error.
1 policies evaluated.
## Policy 1: Passthrough.sentinel (soft-mandatory)
Result: false
FALSE - Passthrough.sentinel:1:1 - Rule "main"
`

View File

@ -277,6 +277,7 @@ in order to capture the filesystem context the remote workspace expects:
ConfigurationVersion: cv,
Refresh: tfe.Bool(op.PlanRefresh),
Workspace: w,
AutoApply: tfe.Bool(op.AutoApprove),
}
switch op.PlanMode {

View File

@ -0,0 +1,280 @@
//go:build e2e
// +build e2e
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"testing"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/internal/e2e"
)
type tfCommand struct {
command []string
expectedOutput string
expectedErr string
}
func Test_terraform_apply_autoApprove(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
setup func(t *testing.T) (map[string]string, func())
commands []tfCommand
validations func(t *testing.T, orgName, wsName string)
}{
"workspace manual apply, terraform apply without auto-approve": {
setup: func(t *testing.T) (map[string]string, func()) {
org, orgCleanup := createOrganization(t)
wOpts := tfe.WorkspaceCreateOptions{
Name: tfe.String(randomString(t)),
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(false),
}
workspace := createWorkspace(t, org, wOpts)
cleanup := func() {
defer orgCleanup()
}
names := map[string]string{
"organization": org.Name,
"workspace": workspace.Name,
}
return names, cleanup
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: "Terraform has been successfully initialized",
expectedErr: "",
},
{
command: []string{"apply"},
expectedOutput: "Do you want to perform these actions in workspace",
expectedErr: "Error asking approve",
},
},
validations: func(t *testing.T, orgName, wsName string) {
workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"})
if err != nil {
t.Fatal(err)
}
if workspace.CurrentRun == nil {
t.Fatal("Expected workspace to have run, but got nil")
}
if workspace.CurrentRun.Status != tfe.RunPlanned {
t.Fatalf("Expected run status to be `planned`, but is %s", workspace.CurrentRun.Status)
}
},
},
"workspace auto apply, terraform apply without auto-approve": {
setup: func(t *testing.T) (map[string]string, func()) {
org, orgCleanup := createOrganization(t)
wOpts := tfe.WorkspaceCreateOptions{
Name: tfe.String(randomString(t)),
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true),
}
workspace := createWorkspace(t, org, wOpts)
cleanup := func() {
defer orgCleanup()
}
names := map[string]string{
"organization": org.Name,
"workspace": workspace.Name,
}
return names, cleanup
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: "Terraform has been successfully initialized",
expectedErr: "",
},
{
command: []string{"apply"},
expectedOutput: "Do you want to perform these actions in workspace",
expectedErr: "Error asking approve",
},
},
validations: func(t *testing.T, orgName, wsName string) {
workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"})
if err != nil {
t.Fatal(err)
}
if workspace.CurrentRun == nil {
t.Fatalf("Expected workspace to have run, but got nil")
}
if workspace.CurrentRun.Status != tfe.RunPlanned {
t.Fatalf("Expected run status to be `planned`, but is %s", workspace.CurrentRun.Status)
}
},
},
"workspace manual apply, terraform apply auto-approve": {
setup: func(t *testing.T) (map[string]string, func()) {
org, orgCleanup := createOrganization(t)
wOpts := tfe.WorkspaceCreateOptions{
Name: tfe.String(randomString(t)),
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(false),
}
workspace := createWorkspace(t, org, wOpts)
cleanup := func() {
defer orgCleanup()
}
names := map[string]string{
"organization": org.Name,
"workspace": workspace.Name,
}
return names, cleanup
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: "Terraform has been successfully initialized",
expectedErr: "",
},
{
command: []string{"apply", "-auto-approve"},
expectedOutput: "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.",
expectedErr: "",
},
},
validations: func(t *testing.T, orgName, wsName string) {
workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"})
if err != nil {
t.Fatal(err)
}
if workspace.CurrentRun == nil {
t.Fatalf("Expected workspace to have run, but got nil")
}
if workspace.CurrentRun.Status != tfe.RunApplied {
t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status)
}
},
},
"workspace auto apply, terraform apply auto-approve": {
setup: func(t *testing.T) (map[string]string, func()) {
org, orgCleanup := createOrganization(t)
wOpts := tfe.WorkspaceCreateOptions{
Name: tfe.String(randomString(t)),
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true),
}
workspace := createWorkspace(t, org, wOpts)
cleanup := func() {
defer orgCleanup()
}
names := map[string]string{
"organization": org.Name,
"workspace": workspace.Name,
}
return names, cleanup
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: "Terraform has been successfully initialized",
expectedErr: "",
},
{
command: []string{"apply", "-auto-approve"},
expectedOutput: "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.",
expectedErr: "",
},
},
validations: func(t *testing.T, orgName, wsName string) {
workspace, err := tfeClient.Workspaces.ReadWithOptions(ctx, orgName, wsName, &tfe.WorkspaceReadOptions{Include: "current_run"})
if err != nil {
t.Fatal(err)
}
if workspace.CurrentRun == nil {
t.Fatalf("Expected workspace to have run, but got nil")
}
if workspace.CurrentRun.Status != tfe.RunApplied {
t.Fatalf("Expected run status to be `applied`, but is %s", workspace.CurrentRun.Status)
}
},
},
}
for name, tc := range cases {
log.Println("Test: ", name)
resourceData, cleanup := tc.setup(t)
defer cleanup()
tmpDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
t.Fatal(err)
}
orgName := resourceData["organization"]
wsName := resourceData["workspace"]
tfBlock := createTerraformBlock(orgName, wsName)
writeMainTF(t, tfBlock, tmpDir)
tf := e2e.NewBinary(terraformBin, tmpDir)
defer tf.Close()
tf.AddEnv("TF_LOG=debug")
tf.AddEnv(cliConfigFileEnv)
for _, cmd := range tc.commands {
stdout, stderr, err := tf.Run(cmd.command...)
if cmd.expectedErr == "" && err != nil {
t.Fatalf("Expected no error, but got %v. stderr\n: %s", err, stderr)
}
if cmd.expectedErr != "" {
if !strings.Contains(stderr, cmd.expectedErr) {
t.Fatalf("Expected to find error %s, but got %s", cmd.expectedErr, stderr)
}
}
if cmd.expectedOutput != "" && !strings.Contains(stdout, cmd.expectedOutput) {
t.Fatalf("Expected to find output %s, but did not find in\n%s", cmd.expectedOutput, stdout)
}
}
tc.validations(t, orgName, wsName)
}
}
func createTerraformBlock(org, ws string) string {
return fmt.Sprintf(
`terraform {
cloud {
hostname = "%s"
organization = "%s"
workspaces {
name = "%s"
}
}
}
resource "random_pet" "server" {
keepers = {
uuid = uuid()
}
length = 3
}`, tfeHostname, org, ws)
}
func writeMainTF(t *testing.T, block string, dir string) {
f, err := os.Create(fmt.Sprintf("%s/main.tf", dir))
if err != nil {
t.Fatal(err)
}
_, err = f.WriteString(block)
if err != nil {
t.Fatal(err)
}
f.Close()
}

View File

@ -0,0 +1,50 @@
//go:build e2e
// +build e2e
package main
import (
"context"
"fmt"
"testing"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/go-uuid"
)
func createOrganization(t *testing.T) (*tfe.Organization, func()) {
ctx := context.Background()
org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
Name: tfe.String("tst-" + randomString(t)),
Email: tfe.String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
if err != nil {
t.Fatal(err)
}
return org, func() {
if err := tfeClient.Organizations.Delete(ctx, org.Name); err != nil {
t.Errorf("Error destroying organization! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Organization: %s\nError: %s", org.Name, err)
}
}
}
func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace {
ctx := context.Background()
w, err := tfeClient.Workspaces.Create(ctx, org.Name, wOpts)
if err != nil {
t.Fatal(err)
}
return w
}
func randomString(t *testing.T) string {
v, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
return v
}

View File

@ -0,0 +1,198 @@
//go:build e2e
// +build e2e
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"testing"
tfe "github.com/hashicorp/go-tfe"
)
var terraformVersion string
var terraformBin string
var cliConfigFileEnv string
var tfeClient *tfe.Client
var tfeHostname string
var tfeToken string
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()
os.Exit(code)
}
func accTest() bool {
// TF_ACC is set when we want to run acceptance tests, meaning it relies on
// network access.
return os.Getenv("TF_ACC") != ""
}
func setup() func() {
setTfeClient()
teardown := setupBinary()
setVersion()
ensureVersionExists()
return func() {
teardown()
}
}
func setTfeClient() {
hostname := os.Getenv("TFE_HOSTNAME")
token := os.Getenv("TFE_TOKEN")
if hostname == "" {
log.Fatalf("hostname cannot be empty")
}
if token == "" {
log.Fatalf("token cannot be empty")
}
tfeHostname = hostname
tfeToken = token
cfg := &tfe.Config{
Address: fmt.Sprintf("https://%s", hostname),
Token: token,
}
// Create a new TFE client.
client, err := tfe.NewClient(cfg)
if err != nil {
log.Fatal(err)
}
tfeClient = client
}
func setupBinary() func() {
log.Println("Setting up terraform binary")
tmpTerraformBinaryDir, err := ioutil.TempDir("", "terraform-test")
if err != nil {
log.Fatal(err)
}
log.Println(tmpTerraformBinaryDir)
currentDir, err := os.Getwd()
defer os.Chdir(currentDir)
if err != nil {
log.Fatal(err)
}
// Getting top level dir
dirPaths := strings.Split(currentDir, "/")
log.Println(currentDir)
topLevel := len(dirPaths) - 3
topDir := strings.Join(dirPaths[0:topLevel], "/")
if err := os.Chdir(topDir); err != nil {
log.Fatal(err)
}
cmd := exec.Command("go", "build", "-o", tmpTerraformBinaryDir)
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
credFile := fmt.Sprintf("%s/dev.tfrc", tmpTerraformBinaryDir)
writeCredRC(credFile)
terraformBin = fmt.Sprintf("%s/terraform", tmpTerraformBinaryDir)
cliConfigFileEnv = fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", credFile)
return func() {
os.RemoveAll(tmpTerraformBinaryDir)
}
}
func setVersion() {
log.Println("Retrieving version")
cmd := exec.Command(terraformBin, "version", "-json")
out, err := cmd.Output()
if err != nil {
log.Fatal(fmt.Sprintf("Could not output terraform version: %v", err))
}
var data map[string]interface{}
if err := json.Unmarshal(out, &data); err != nil {
log.Fatal(fmt.Sprintf("Could not unmarshal version output: %v", err))
}
out, err = exec.Command("git", "rev-parse", "HEAD").Output()
if err != nil {
log.Fatal(fmt.Sprintf("Could not execute go build command: %v", err))
}
hash := string(out)[0:8]
terraformVersion = fmt.Sprintf("%s-%s", data["terraform_version"].(string), hash)
}
func ensureVersionExists() {
opts := tfe.AdminTerraformVersionsListOptions{
ListOptions: tfe.ListOptions{
PageNumber: 1,
PageSize: 100,
},
}
hasVersion := false
findTfVersion:
for {
tfVersionList, err := tfeClient.Admin.TerraformVersions.List(context.Background(), opts)
if err != nil {
log.Fatalf("Could not retrieve list of terraform versions: %v", err)
}
for _, item := range tfVersionList.Items {
if item.Version == terraformVersion {
hasVersion = true
break findTfVersion
}
}
// Exit the loop when we've seen all pages.
if tfVersionList.CurrentPage >= tfVersionList.TotalPages {
break
}
// Update the page number to get the next page.
opts.PageNumber = tfVersionList.NextPage
}
if !hasVersion {
log.Fatalf("Terraform Version %s does not exist in the list. Please add it.", terraformVersion)
}
}
func writeCredRC(file string) {
creds := credentialBlock()
f, err := os.Create(file)
if err != nil {
log.Fatal(err)
}
_, err = f.WriteString(creds)
if err != nil {
log.Fatal(err)
}
f.Close()
}
func credentialBlock() string {
return fmt.Sprintf(`
credentials "%s" {
token = "%s"
}`, tfeHostname, tfeToken)
}

View File

@ -1353,7 +1353,7 @@ func (m *MockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string)
panic("not implemented")
}
func (m *MockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string) (*tfe.WorkspaceList, error) {
func (m *MockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string, options *tfe.RemoteStateConsumersListOptions) (*tfe.WorkspaceList, error) {
panic("not implemented")
}