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/davecgh/go-spew v1.1.1
|
||||||
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
|
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
|
||||||
github.com/go-test/deep v1.0.3
|
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/golang/protobuf v1.5.2
|
||||||
github.com/google/go-cmp v0.5.5
|
github.com/google/go-cmp v0.5.5
|
||||||
github.com/google/uuid v1.2.0
|
github.com/google/uuid v1.2.0
|
||||||
|
@ -40,7 +40,7 @@ require (
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-plugin v1.4.3
|
github.com/hashicorp/go-plugin v1.4.3
|
||||||
github.com/hashicorp/go-retryablehttp v0.5.2
|
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-uuid v1.0.1
|
||||||
github.com/hashicorp/go-version v1.2.1
|
github.com/hashicorp/go-version v1.2.1
|
||||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
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.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.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.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.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 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.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.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 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
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-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.19.1-0.20210922134841-a2c1784e9c00 h1:51ARk47jO4piKzhhbwk6u67ErvSuBj4cu2f2VS9HkgI=
|
||||||
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/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.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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
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-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/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.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.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
|
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
|
||||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||||
|
|
|
@ -181,72 +181,40 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if the run cannot be confirmed.
|
// Return if the run cannot be confirmed.
|
||||||
if !w.AutoApply && !r.Actions.IsConfirmable {
|
if !op.AutoApprove && !r.Actions.IsConfirmable {
|
||||||
return r, nil
|
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
|
mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove
|
||||||
|
|
||||||
if !w.AutoApply {
|
if mustConfirm {
|
||||||
if mustConfirm {
|
opts := &terraform.InputOpts{Id: "approve"}
|
||||||
opts := &terraform.InputOpts{Id: "approve"}
|
|
||||||
|
|
||||||
if op.PlanMode == plans.DestroyMode {
|
if op.PlanMode == plans.DestroyMode {
|
||||||
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
|
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" +
|
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
|
||||||
"There is no undo. Only 'yes' will be accepted to confirm."
|
"There is no undo. Only 'yes' will be accepted to confirm."
|
||||||
} else {
|
} else {
|
||||||
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
|
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
|
||||||
opts.Description = "Terraform will perform the actions described above.\n" +
|
opts.Description = "Terraform will perform the actions described above.\n" +
|
||||||
"Only 'yes' will be accepted to approve."
|
"Only 'yes' will be accepted to approve."
|
||||||
}
|
|
||||||
|
|
||||||
err = b.confirm(stopCtx, op, opts, r, "yes")
|
|
||||||
if err != nil && err != errRunApproved {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != errRunApproved {
|
err = b.confirm(stopCtx, op, opts, r, "yes")
|
||||||
if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
|
if err != nil && err != errRunApproved {
|
||||||
return r, generalError("Failed to approve the apply command", err)
|
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
|
if !op.AutoApprove && err != errRunApproved {
|
||||||
// line to separate the ouputs.
|
if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
|
||||||
if w.AutoApply || !mustConfirm {
|
return r, generalError("Failed to approve the apply command", err)
|
||||||
if b.CLI != nil {
|
|
||||||
b.CLI.Output("")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cloud
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
tfe "github.com/hashicorp/go-tfe"
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
|
@ -697,6 +699,14 @@ func TestCloud_applyNoApprove(t *testing.T) {
|
||||||
func TestCloud_applyAutoApprove(t *testing.T) {
|
func TestCloud_applyAutoApprove(t *testing.T) {
|
||||||
b, bCleanup := testBackendWithName(t)
|
b, bCleanup := testBackendWithName(t)
|
||||||
defer bCleanup()
|
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")
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
||||||
defer configCleanup()
|
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)
|
b, bCleanup := testBackendWithPrefix(t)
|
||||||
defer bCleanup()
|
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.
|
// Create a named workspace that auto applies.
|
||||||
_, err := b.client.Workspaces.Create(
|
_, err := b.client.Workspaces.Create(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
b.organization,
|
b.organization,
|
||||||
tfe.WorkspaceCreateOptions{
|
tfe.WorkspaceCreateOptions{
|
||||||
AutoApply: tfe.Bool(true),
|
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
|
||||||
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -916,6 +933,7 @@ func TestCloud_applyWithAutoApply(t *testing.T) {
|
||||||
op.UIIn = input
|
op.UIIn = input
|
||||||
op.UIOut = b.CLI
|
op.UIOut = b.CLI
|
||||||
op.Workspace = "prod"
|
op.Workspace = "prod"
|
||||||
|
op.AutoApprove = true
|
||||||
|
|
||||||
run, err := b.Operation(context.Background(), op)
|
run, err := b.Operation(context.Background(), op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1374,6 +1392,34 @@ func TestCloud_applyPolicySoftFail(t *testing.T) {
|
||||||
func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
|
func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
|
||||||
b, bCleanup := testBackendWithName(t)
|
b, bCleanup := testBackendWithName(t)
|
||||||
defer bCleanup()
|
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")
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
||||||
defer configCleanup()
|
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)
|
b, bCleanup := testBackendWithName(t)
|
||||||
defer bCleanup()
|
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.
|
// Create a named workspace that auto applies.
|
||||||
_, err := b.client.Workspaces.Create(
|
_, err := b.client.Workspaces.Create(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
b.organization,
|
b.organization,
|
||||||
tfe.WorkspaceCreateOptions{
|
tfe.WorkspaceCreateOptions{
|
||||||
AutoApply: tfe.Bool(true),
|
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
|
||||||
Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1451,6 +1504,7 @@ func TestCloud_applyPolicySoftFailAutoApply(t *testing.T) {
|
||||||
op.UIIn = input
|
op.UIIn = input
|
||||||
op.UIOut = b.CLI
|
op.UIOut = b.CLI
|
||||||
op.Workspace = "prod"
|
op.Workspace = "prod"
|
||||||
|
op.AutoApprove = true
|
||||||
|
|
||||||
run, err := b.Operation(context.Background(), op)
|
run, err := b.Operation(context.Background(), op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1465,7 +1519,7 @@ func TestCloud_applyPolicySoftFailAutoApply(t *testing.T) {
|
||||||
t.Fatalf("expected a non-empty plan")
|
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)
|
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,
|
ConfigurationVersion: cv,
|
||||||
Refresh: tfe.Bool(op.PlanRefresh),
|
Refresh: tfe.Bool(op.PlanRefresh),
|
||||||
Workspace: w,
|
Workspace: w,
|
||||||
|
AutoApply: tfe.Bool(op.AutoApprove),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch op.PlanMode {
|
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")
|
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")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue