Add auto-approve logic, e2e tests
This commit is contained in:
parent
ee384e8716
commit
a387af6c61
4
go.mod
4
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -181,41 +181,12 @@ 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"}
|
||||
|
||||
|
@ -233,23 +204,20 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
|
|||
if err != nil && err != errRunApproved {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w)
|
||||
if err != nil {
|
||||
return r, err
|
||||
|
|
|
@ -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,16 +898,23 @@ 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"),
|
||||
},
|
||||
)
|
||||
|
@ -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,16 +1468,23 @@ 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"),
|
||||
},
|
||||
)
|
||||
|
@ -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"
|
||||
`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue