Merge pull request #19378 from hashicorp/svh/f-remote-state
backend/remote: change how to fall back to a local backend
This commit is contained in:
commit
ae6ddc29ae
|
@ -3,7 +3,6 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
|
@ -49,13 +48,7 @@ func Init(services *disco.Disco) {
|
|||
backends = map[string]backend.InitFn{
|
||||
// Enhanced backends.
|
||||
"local": func() backend.Backend { return backendLocal.New() },
|
||||
"remote": func() backend.Backend {
|
||||
b := backendRemote.New(services)
|
||||
if os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" {
|
||||
return backendLocal.NewWithBackend(b)
|
||||
}
|
||||
return b
|
||||
},
|
||||
"remote": func() backend.Backend { return backendRemote.New(services) },
|
||||
|
||||
// Remote State backends.
|
||||
"artifactory": func() backend.Backend { return backendArtifactory.New() },
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||
)
|
||||
|
||||
func TestInit_backend(t *testing.T) {
|
||||
|
@ -44,42 +41,3 @@ func TestInit_backend(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit_forceLocalBackend(t *testing.T) {
|
||||
// Initialize the backends map
|
||||
Init(nil)
|
||||
|
||||
enhancedBackends := []struct {
|
||||
Name string
|
||||
Type string
|
||||
}{
|
||||
{"local", "nil"},
|
||||
{"remote", "*remote.Remote"},
|
||||
}
|
||||
|
||||
// Set the TF_FORCE_LOCAL_BACKEND flag so all enhanced backends will
|
||||
// return a local.Local backend with themselves as embedded backend.
|
||||
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
||||
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
||||
}
|
||||
defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
||||
|
||||
// Make sure we always get the local backend.
|
||||
for _, b := range enhancedBackends {
|
||||
f := Backend(b.Name)
|
||||
|
||||
local, ok := f().(*backendLocal.Local)
|
||||
if !ok {
|
||||
t.Fatalf("expected backend %q to be \"*local.Local\", got: %T", b.Name, f())
|
||||
}
|
||||
|
||||
bType := "nil"
|
||||
if local.Backend != nil {
|
||||
bType = reflect.TypeOf(local.Backend).String()
|
||||
}
|
||||
|
||||
if bType != b.Type {
|
||||
t.Fatalf("expected local.Backend to be %s, got: %s", b.Type, bType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,9 +258,6 @@ func (b *Local) DeleteWorkspace(name string) error {
|
|||
}
|
||||
|
||||
func (b *Local) StateMgr(name string) (statemgr.Full, error) {
|
||||
statePath, stateOutPath, backupPath := b.StatePaths(name)
|
||||
log.Printf("[TRACE] backend/local: state manager for workspace %q will:\n - read initial snapshot from %s\n - write new snapshots to %s\n - create any backup at %s", name, statePath, stateOutPath, backupPath)
|
||||
|
||||
// If we have a backend handling state, delegate to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.StateMgr(name)
|
||||
|
@ -274,6 +271,9 @@ func (b *Local) StateMgr(name string) (statemgr.Full, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
statePath, stateOutPath, backupPath := b.StatePaths(name)
|
||||
log.Printf("[TRACE] backend/local: state manager for workspace %q will:\n - read initial snapshot from %s\n - write new snapshots to %s\n - create any backup at %s", name, statePath, stateOutPath, backupPath)
|
||||
|
||||
s := statemgr.NewFilesystemBetweenPaths(statePath, stateOutPath)
|
||||
if backupPath != "" {
|
||||
s.SetBackupPath(backupPath)
|
||||
|
|
|
@ -25,8 +25,8 @@ import (
|
|||
func TestLocal_applyBasic(t *testing.T) {
|
||||
b, cleanup := TestLocal(t)
|
||||
defer cleanup()
|
||||
p := TestLocalProvider(t, b, "test", applyFixtureSchema())
|
||||
|
||||
p := TestLocalProvider(t, b, "test", applyFixtureSchema())
|
||||
p.ApplyResourceChangeResponse = providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("yes"),
|
||||
"ami": cty.StringVal("bar"),
|
||||
|
@ -95,8 +95,8 @@ func TestLocal_applyEmptyDir(t *testing.T) {
|
|||
func TestLocal_applyEmptyDirDestroy(t *testing.T) {
|
||||
b, cleanup := TestLocal(t)
|
||||
defer cleanup()
|
||||
p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
|
||||
|
||||
p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
|
||||
p.ApplyResourceChangeResponse = providers.ApplyResourceChangeResponse{}
|
||||
|
||||
op, configCleanup := testOperationApply(t, "./test-fixtures/empty")
|
||||
|
@ -122,6 +122,7 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) {
|
|||
func TestLocal_applyError(t *testing.T) {
|
||||
b, cleanup := TestLocal(t)
|
||||
defer cleanup()
|
||||
|
||||
p := TestLocalProvider(t, b, "test", nil)
|
||||
p.GetSchemaReturn = &terraform.ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
|
|
|
@ -188,7 +188,6 @@ func (b *Local) opPlan(
|
|||
}
|
||||
|
||||
func (b *Local) renderPlan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
|
||||
counts := map[plans.Action]int{}
|
||||
for _, change := range plan.Changes.Resources {
|
||||
counts[change.Action]++
|
||||
|
|
|
@ -25,6 +25,8 @@ import (
|
|||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -49,28 +51,36 @@ type Remote struct {
|
|||
// Operation. See Operation for more details.
|
||||
ContextOpts *terraform.ContextOpts
|
||||
|
||||
// client is the remote backend API client
|
||||
// client is the remote backend API client.
|
||||
client *tfe.Client
|
||||
|
||||
// hostname of the remote backend server
|
||||
// hostname of the remote backend server.
|
||||
hostname string
|
||||
|
||||
// organization is the organization that contains the target workspaces
|
||||
// organization is the organization that contains the target workspaces.
|
||||
organization string
|
||||
|
||||
// workspace is used to map the default workspace to a remote workspace
|
||||
// workspace is used to map the default workspace to a remote workspace.
|
||||
workspace string
|
||||
|
||||
// prefix is used to filter down a set of workspaces that use a single
|
||||
// prefix is used to filter down a set of workspaces that use a single.
|
||||
// configuration
|
||||
prefix string
|
||||
|
||||
// schema defines the configuration for the backend
|
||||
// schema defines the configuration for the backend.
|
||||
schema *schema.Backend
|
||||
|
||||
// services is used for service discovery
|
||||
services *disco.Disco
|
||||
|
||||
// local, if non-nil, will be used for all enhanced behavior. This
|
||||
// allows local behavior with the remote backend functioning as remote
|
||||
// state storage backend.
|
||||
local backend.Enhanced
|
||||
|
||||
// forceLocal, if true, will force the use of the local backend.
|
||||
forceLocal bool
|
||||
|
||||
// opLock locks operations
|
||||
opLock sync.Mutex
|
||||
}
|
||||
|
@ -84,6 +94,7 @@ func New(services *disco.Disco) *Remote {
|
|||
}
|
||||
}
|
||||
|
||||
// ConfigSchema implements backend.Enhanced.
|
||||
func (b *Remote) ConfigSchema() *configschema.Block {
|
||||
return &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
|
@ -126,6 +137,7 @@ func (b *Remote) ConfigSchema() *configschema.Block {
|
|||
}
|
||||
}
|
||||
|
||||
// ValidateConfig implements backend.Enhanced.
|
||||
func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
|
@ -173,6 +185,7 @@ func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
// Configure implements backend.Enhanced.
|
||||
func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
|
@ -255,8 +268,31 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
`Terraform Enterprise client: %s.`, err,
|
||||
),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
// Check if the organization exists.
|
||||
_, err = b.client.Organizations.Read(context.Background(), b.organization)
|
||||
if err != nil {
|
||||
if err == tfe.ErrResourceNotFound {
|
||||
err = fmt.Errorf("organization %s does not exist", b.organization)
|
||||
}
|
||||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Failed to read organization settings",
|
||||
fmt.Sprintf(
|
||||
`The "remote" backend encountered an unexpected error while reading the `+
|
||||
`organization settings: %s.`, err,
|
||||
),
|
||||
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
// Configure a local backend for when we need to run operations locally.
|
||||
b.local = backendLocal.NewWithBackend(b)
|
||||
b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != ""
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
|
@ -292,103 +328,7 @@ func (b *Remote) token(hostname string) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
// Workspaces returns a filtered list of remote workspace names.
|
||||
func (b *Remote) Workspaces() ([]string, error) {
|
||||
if b.prefix == "" {
|
||||
return nil, backend.ErrWorkspacesNotSupported
|
||||
}
|
||||
return b.workspaces()
|
||||
}
|
||||
|
||||
func (b *Remote) workspaces() ([]string, error) {
|
||||
// Check if the configured organization exists.
|
||||
_, err := b.client.Organizations.Read(context.Background(), b.organization)
|
||||
if err != nil {
|
||||
if err == tfe.ErrResourceNotFound {
|
||||
return nil, fmt.Errorf("organization %s does not exist", b.organization)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := tfe.WorkspaceListOptions{}
|
||||
switch {
|
||||
case b.workspace != "":
|
||||
options.Search = tfe.String(b.workspace)
|
||||
case b.prefix != "":
|
||||
options.Search = tfe.String(b.prefix)
|
||||
}
|
||||
|
||||
// Create a slice to contain all the names.
|
||||
var names []string
|
||||
|
||||
for {
|
||||
wl, err := b.client.Workspaces.List(context.Background(), b.organization, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, w := range wl.Items {
|
||||
if b.workspace != "" && w.Name == b.workspace {
|
||||
names = append(names, backend.DefaultStateName)
|
||||
continue
|
||||
}
|
||||
if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) {
|
||||
names = append(names, strings.TrimPrefix(w.Name, b.prefix))
|
||||
}
|
||||
}
|
||||
|
||||
// Exit the loop when we've seen all pages.
|
||||
if wl.CurrentPage >= wl.TotalPages {
|
||||
break
|
||||
}
|
||||
|
||||
// Update the page number to get the next page.
|
||||
options.PageNumber = wl.NextPage
|
||||
}
|
||||
|
||||
// Sort the result so we have consistent output.
|
||||
sort.StringSlice(names).Sort()
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// DeleteWorkspace removes the remote workspace if it exists.
|
||||
func (b *Remote) DeleteWorkspace(name string) error {
|
||||
if b.workspace == "" && name == backend.DefaultStateName {
|
||||
return backend.ErrDefaultWorkspaceNotSupported
|
||||
}
|
||||
if b.prefix == "" && name != backend.DefaultStateName {
|
||||
return backend.ErrWorkspacesNotSupported
|
||||
}
|
||||
|
||||
// Configure the remote workspace name.
|
||||
switch {
|
||||
case name == backend.DefaultStateName:
|
||||
name = b.workspace
|
||||
case b.prefix != "" && !strings.HasPrefix(name, b.prefix):
|
||||
name = b.prefix + name
|
||||
}
|
||||
|
||||
// Check if the configured organization exists.
|
||||
_, err := b.client.Organizations.Read(context.Background(), b.organization)
|
||||
if err != nil {
|
||||
if err == tfe.ErrResourceNotFound {
|
||||
return fmt.Errorf("organization %s does not exist", b.organization)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
client := &remoteClient{
|
||||
client: b.client,
|
||||
organization: b.organization,
|
||||
workspace: name,
|
||||
}
|
||||
|
||||
return client.Delete()
|
||||
}
|
||||
|
||||
// StateMgr returns the latest state of the given remote workspace. The
|
||||
// workspace will be created if it doesn't exist.
|
||||
// StateMgr implements backend.Enhanced.
|
||||
func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||
if b.workspace == "" && name == backend.DefaultStateName {
|
||||
return nil, backend.ErrDefaultWorkspaceNotSupported
|
||||
|
@ -447,18 +387,111 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
|||
return &remote.State{Client: client}, nil
|
||||
}
|
||||
|
||||
// Operation implements backend.Enhanced
|
||||
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
||||
// Configure the remote workspace name.
|
||||
switch {
|
||||
case op.Workspace == backend.DefaultStateName:
|
||||
op.Workspace = b.workspace
|
||||
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
|
||||
op.Workspace = b.prefix + op.Workspace
|
||||
// DeleteWorkspace implements backend.Enhanced.
|
||||
func (b *Remote) DeleteWorkspace(name string) error {
|
||||
if b.workspace == "" && name == backend.DefaultStateName {
|
||||
return backend.ErrDefaultWorkspaceNotSupported
|
||||
}
|
||||
if b.prefix == "" && name != backend.DefaultStateName {
|
||||
return backend.ErrWorkspacesNotSupported
|
||||
}
|
||||
|
||||
// Configure the remote workspace name.
|
||||
switch {
|
||||
case name == backend.DefaultStateName:
|
||||
name = b.workspace
|
||||
case b.prefix != "" && !strings.HasPrefix(name, b.prefix):
|
||||
name = b.prefix + name
|
||||
}
|
||||
|
||||
client := &remoteClient{
|
||||
client: b.client,
|
||||
organization: b.organization,
|
||||
workspace: name,
|
||||
}
|
||||
|
||||
return client.Delete()
|
||||
}
|
||||
|
||||
// Workspaces implements backend.Enhanced.
|
||||
func (b *Remote) Workspaces() ([]string, error) {
|
||||
if b.prefix == "" {
|
||||
return nil, backend.ErrWorkspacesNotSupported
|
||||
}
|
||||
return b.workspaces()
|
||||
}
|
||||
|
||||
// workspaces returns a filtered list of remote workspace names.
|
||||
func (b *Remote) workspaces() ([]string, error) {
|
||||
options := tfe.WorkspaceListOptions{}
|
||||
switch {
|
||||
case b.workspace != "":
|
||||
options.Search = tfe.String(b.workspace)
|
||||
case b.prefix != "":
|
||||
options.Search = tfe.String(b.prefix)
|
||||
}
|
||||
|
||||
// Create a slice to contain all the names.
|
||||
var names []string
|
||||
|
||||
for {
|
||||
wl, err := b.client.Workspaces.List(context.Background(), b.organization, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, w := range wl.Items {
|
||||
if b.workspace != "" && w.Name == b.workspace {
|
||||
names = append(names, backend.DefaultStateName)
|
||||
continue
|
||||
}
|
||||
if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) {
|
||||
names = append(names, strings.TrimPrefix(w.Name, b.prefix))
|
||||
}
|
||||
}
|
||||
|
||||
// Exit the loop when we've seen all pages.
|
||||
if wl.CurrentPage >= wl.TotalPages {
|
||||
break
|
||||
}
|
||||
|
||||
// Update the page number to get the next page.
|
||||
options.PageNumber = wl.NextPage
|
||||
}
|
||||
|
||||
// Sort the result so we have consistent output.
|
||||
sort.StringSlice(names).Sort()
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Operation implements backend.Enhanced.
|
||||
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
||||
// Get the remote workspace name.
|
||||
workspace := op.Workspace
|
||||
switch {
|
||||
case op.Workspace == backend.DefaultStateName:
|
||||
workspace = b.workspace
|
||||
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
|
||||
workspace = b.prefix + op.Workspace
|
||||
}
|
||||
|
||||
// Retrieve the workspace for this operation.
|
||||
w, err := b.client.Workspaces.Read(ctx, b.organization, workspace)
|
||||
if err != nil {
|
||||
return nil, generalError("Failed to retrieve workspace", err)
|
||||
}
|
||||
|
||||
// Check if we need to use the local backend to run the operation.
|
||||
if b.forceLocal || !w.Operations {
|
||||
return b.local.Operation(ctx, op)
|
||||
}
|
||||
|
||||
// Set the remote workspace name.
|
||||
op.Workspace = w.Name
|
||||
|
||||
// Determine the function to call for our operation
|
||||
var f func(context.Context, context.Context, *backend.Operation) (*tfe.Run, error)
|
||||
var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error)
|
||||
switch op.Type {
|
||||
case backend.OperationTypePlan:
|
||||
f = b.opPlan
|
||||
|
@ -499,7 +532,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
|||
|
||||
defer b.opLock.Unlock()
|
||||
|
||||
r, opErr := f(stopCtx, cancelCtx, op)
|
||||
r, opErr := f(stopCtx, cancelCtx, op, w)
|
||||
if opErr != nil && opErr != context.Canceled {
|
||||
b.ReportResult(runningOp, opErr)
|
||||
return
|
||||
|
|
|
@ -12,15 +12,9 @@ import (
|
|||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation) (*tfe.Run, error) {
|
||||
func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
|
||||
log.Printf("[INFO] backend/remote: starting Apply operation")
|
||||
|
||||
// Retrieve the workspace used to run this operation in.
|
||||
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
|
||||
if err != nil {
|
||||
return nil, generalError("Failed to retrieve workspace", err)
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if !w.Permissions.CanUpdate {
|
||||
|
|
|
@ -64,11 +64,14 @@ func TestRemote_applyBasic(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,11 +410,14 @@ func TestRemote_applyAutoApprove(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -460,11 +466,120 @@ func TestRemote_applyWithAutoApply(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_applyForceLocal(t *testing.T) {
|
||||
// Set TF_FORCE_LOCAL_BACKEND so the remote backend will use
|
||||
// the local backend with itself as embedded backend.
|
||||
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
||||
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
||||
}
|
||||
defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
||||
|
||||
b := testBackendDefault(t)
|
||||
|
||||
op, configCleanup := testOperationApply(t, "./test-fixtures/apply")
|
||||
defer configCleanup()
|
||||
|
||||
input := testInput(t, map[string]string{
|
||||
"approve": "yes",
|
||||
})
|
||||
|
||||
op.UIIn = input
|
||||
op.UIOut = b.CLI
|
||||
op.Workspace = backend.DefaultStateName
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting operation: %v", err)
|
||||
}
|
||||
|
||||
<-run.Done()
|
||||
if run.Result != backend.OperationSuccess {
|
||||
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
||||
}
|
||||
if run.PlanEmpty {
|
||||
t.Fatalf("expected a non-empty plan")
|
||||
}
|
||||
|
||||
if len(input.answers) > 0 {
|
||||
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
||||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("unexpected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) {
|
||||
b := testBackendNoDefault(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a named workspace that doesn't allow operations.
|
||||
_, err := b.client.Workspaces.Create(
|
||||
ctx,
|
||||
b.organization,
|
||||
tfe.WorkspaceCreateOptions{
|
||||
Name: tfe.String(b.prefix + "no-operations"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating named workspace: %v", err)
|
||||
}
|
||||
|
||||
op, configCleanup := testOperationApply(t, "./test-fixtures/apply")
|
||||
defer configCleanup()
|
||||
|
||||
input := testInput(t, map[string]string{
|
||||
"approve": "yes",
|
||||
})
|
||||
|
||||
op.UIIn = input
|
||||
op.UIOut = b.CLI
|
||||
op.Workspace = "no-operations"
|
||||
|
||||
run, err := b.Operation(ctx, op)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting operation: %v", err)
|
||||
}
|
||||
|
||||
<-run.Done()
|
||||
if run.Result != backend.OperationSuccess {
|
||||
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
||||
}
|
||||
if run.PlanEmpty {
|
||||
t.Fatalf("expected a non-empty plan")
|
||||
}
|
||||
|
||||
if len(input.answers) > 0 {
|
||||
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
||||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("unexpected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -526,8 +641,11 @@ func TestRemote_applyLockTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Lock timeout exceeded") {
|
||||
t.Fatalf("missing lock timout error in output: %s", output)
|
||||
t.Fatalf("expected lock timout error in output: %s", output)
|
||||
}
|
||||
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("unexpected plan summery in output: %s", output)
|
||||
|
@ -570,11 +688,14 @@ func TestRemote_applyDestroy(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,14 +764,17 @@ func TestRemote_applyPolicyPass(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: true") {
|
||||
t.Fatalf("missing polic check result in output: %s", output)
|
||||
t.Fatalf("expected polic check result in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -691,11 +815,14 @@ func TestRemote_applyPolicyHardFail(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("unexpected apply summery in output: %s", output)
|
||||
|
@ -735,14 +862,17 @@ func TestRemote_applyPolicySoftFail(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -784,11 +914,14 @@ func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("unexpected apply summery in output: %s", output)
|
||||
|
@ -841,14 +974,17 @@ func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running apply in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||
t.Fatalf("missing apply summery in output: %s", output)
|
||||
t.Fatalf("expected apply summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -875,6 +1011,6 @@ func TestRemote_applyWithRemoteError(t *testing.T) {
|
|||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "null_resource.foo: 1 error") {
|
||||
t.Fatalf("missing apply error in output: %s", output)
|
||||
t.Fatalf("expected apply error in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -907,6 +907,7 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option
|
|||
w := &tfe.Workspace{
|
||||
ID: generateID("ws-"),
|
||||
Name: *options.Name,
|
||||
Operations: !strings.HasSuffix(*options.Name, "no-operations"),
|
||||
Permissions: &tfe.WorkspacePermissions{
|
||||
CanQueueRun: true,
|
||||
CanUpdate: true,
|
||||
|
|
|
@ -18,15 +18,9 @@ import (
|
|||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation) (*tfe.Run, error) {
|
||||
func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
|
||||
log.Printf("[INFO] backend/remote: starting Plan operation")
|
||||
|
||||
// Retrieve the workspace used to run this operation in.
|
||||
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
|
||||
if err != nil {
|
||||
return nil, generalError("Failed to retrieve workspace", err)
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if !w.Permissions.CanQueueRun {
|
||||
|
|
|
@ -54,8 +54,11 @@ func TestRemote_planBasic(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,6 +287,86 @@ func TestRemote_planNoConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRemote_planForceLocal(t *testing.T) {
|
||||
// Set TF_FORCE_LOCAL_BACKEND so the remote backend will use
|
||||
// the local backend with itself as embedded backend.
|
||||
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
||||
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
||||
}
|
||||
defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
||||
|
||||
b := testBackendDefault(t)
|
||||
|
||||
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
||||
defer configCleanup()
|
||||
|
||||
op.Workspace = backend.DefaultStateName
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting operation: %v", err)
|
||||
}
|
||||
|
||||
<-run.Done()
|
||||
if run.Result != backend.OperationSuccess {
|
||||
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
||||
}
|
||||
if run.PlanEmpty {
|
||||
t.Fatalf("expected a non-empty plan")
|
||||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("unexpected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
|
||||
b := testBackendNoDefault(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a named workspace that doesn't allow operations.
|
||||
_, err := b.client.Workspaces.Create(
|
||||
ctx,
|
||||
b.organization,
|
||||
tfe.WorkspaceCreateOptions{
|
||||
Name: tfe.String(b.prefix + "no-operations"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating named workspace: %v", err)
|
||||
}
|
||||
|
||||
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
||||
defer configCleanup()
|
||||
|
||||
op.Workspace = "no-operations"
|
||||
|
||||
run, err := b.Operation(ctx, op)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting operation: %v", err)
|
||||
}
|
||||
|
||||
<-run.Done()
|
||||
if run.Result != backend.OperationSuccess {
|
||||
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
||||
}
|
||||
if run.PlanEmpty {
|
||||
t.Fatalf("expected a non-empty plan")
|
||||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("unexpected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_planLockTimeout(t *testing.T) {
|
||||
b := testBackendDefault(t)
|
||||
ctx := context.Background()
|
||||
|
@ -342,8 +425,11 @@ func TestRemote_planLockTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Lock timeout exceeded") {
|
||||
t.Fatalf("missing lock timout error in output: %s", output)
|
||||
t.Fatalf("expected lock timout error in output: %s", output)
|
||||
}
|
||||
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("unexpected plan summery in output: %s", output)
|
||||
|
@ -428,8 +514,11 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,11 +544,14 @@ func TestRemote_planPolicyPass(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: true") {
|
||||
t.Fatalf("missing polic check result in output: %s", output)
|
||||
t.Fatalf("expected polic check result in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,11 +582,14 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -525,11 +620,14 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||
t.Fatalf("missing plan summery in output: %s", output)
|
||||
t.Fatalf("expected plan summery in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Sentinel Result: false") {
|
||||
t.Fatalf("missing policy check result in output: %s", output)
|
||||
t.Fatalf("expected policy check result in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -555,7 +653,10 @@ func TestRemote_planWithRemoteError(t *testing.T) {
|
|||
}
|
||||
|
||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "null_resource.foo: 1 error") {
|
||||
t.Fatalf("missing plan error in output: %s", output)
|
||||
t.Fatalf("expected plan error in output: %s", output)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||
)
|
||||
|
||||
func TestRemote(t *testing.T) {
|
||||
|
@ -32,6 +34,30 @@ func TestRemote_config(t *testing.T) {
|
|||
confErr string
|
||||
valErr string
|
||||
}{
|
||||
"with_a_nonexisting_organization": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("nonexisting"),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"prefix": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
confErr: "organization nonexisting does not exist",
|
||||
},
|
||||
"with_an_unknown_host": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("nonexisting.local"),
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"prefix": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
confErr: "Host nonexisting.local does not provide a remote backend API",
|
||||
},
|
||||
"with_a_name": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
|
@ -78,18 +104,6 @@ func TestRemote_config(t *testing.T) {
|
|||
}),
|
||||
valErr: `Only one of workspace "name" or "prefix" is allowed`,
|
||||
},
|
||||
"with_an_unknown_host": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("nonexisting.local"),
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"prefix": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
confErr: "Host nonexisting.local does not provide a remote backend API",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
|
@ -107,27 +121,22 @@ func TestRemote_config(t *testing.T) {
|
|||
confDiags := b.Configure(tc.config)
|
||||
if (confDiags.Err() == nil && tc.confErr != "") ||
|
||||
(confDiags.Err() != nil && !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
|
||||
t.Fatalf("%s: unexpected configure result: %v", name, valDiags.Err())
|
||||
t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_nonexistingOrganization(t *testing.T) {
|
||||
msg := "does not exist"
|
||||
func TestRemote_localBackend(t *testing.T) {
|
||||
b := testBackendDefault(t)
|
||||
|
||||
b := testBackendNoDefault(t)
|
||||
b.organization = "nonexisting"
|
||||
|
||||
if _, err := b.StateMgr("prod"); err == nil || !strings.Contains(err.Error(), msg) {
|
||||
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||
local, ok := b.local.(*backendLocal.Local)
|
||||
if !ok {
|
||||
t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local)
|
||||
}
|
||||
|
||||
if err := b.DeleteWorkspace("prod"); err == nil || !strings.Contains(err.Error(), msg) {
|
||||
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||
}
|
||||
|
||||
if _, err := b.Workspaces(); err == nil || !strings.Contains(err.Error(), msg) {
|
||||
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||
remote, ok := local.Backend.(*Remote)
|
||||
if !ok {
|
||||
t.Fatalf("expected local.Backend to be *remote.Remote, got: %T", remote)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,16 @@ import (
|
|||
|
||||
// CLIInit implements backend.CLI
|
||||
func (b *Remote) CLIInit(opts *backend.CLIOpts) error {
|
||||
if cli, ok := b.local.(backend.CLI); ok {
|
||||
if err := cli.CLIInit(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b.CLI = opts.CLI
|
||||
b.CLIColor = opts.CLIColor
|
||||
b.ShowDiagnostics = opts.ShowDiagnostics
|
||||
b.ContextOpts = opts.ContextOpts
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package remote
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/logging"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
if testing.Verbose() {
|
||||
// if we're verbose, use the logging requested by TF_LOG
|
||||
logging.SetOutput()
|
||||
} else {
|
||||
// otherwise silence all logs
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
// Make sure TF_FORCE_LOCAL_BACKEND is unset
|
||||
os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
Terraform v0.11.10
|
||||
|
||||
Initializing plugins and modules...
|
||||
null_resource.hello: Destroying... (ID: 8657651096157629581)
|
||||
null_resource.hello: Destruction complete after 0s
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Terraform v0.11.10
|
||||
|
||||
Initializing plugins and modules...
|
||||
null_resource.hello: Creating...
|
||||
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Terraform v0.11.10
|
||||
|
||||
Initializing plugins and modules...
|
||||
null_resource.hello: Creating...
|
||||
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Terraform v0.11.10
|
||||
|
||||
Initializing plugins and modules...
|
||||
null_resource.hello: Creating...
|
||||
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Terraform v0.11.10
|
||||
|
||||
Initializing plugins and modules...
|
||||
null_resource.hello: Creating...
|
||||
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
tfe "github.com/hashicorp/go-tfe"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/configs"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
"github.com/hashicorp/terraform/svchost"
|
||||
"github.com/hashicorp/terraform/svchost/auth"
|
||||
|
@ -19,6 +21,8 @@ import (
|
|||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -108,6 +112,9 @@ func testBackend(t *testing.T, obj cty.Value) *Remote {
|
|||
}
|
||||
}
|
||||
|
||||
// Set local to a local test backend.
|
||||
b.local = testLocalBackend(t, b)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create the organization.
|
||||
|
@ -131,6 +138,29 @@ func testBackend(t *testing.T, obj cty.Value) *Remote {
|
|||
return b
|
||||
}
|
||||
|
||||
func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced {
|
||||
b := backendLocal.NewWithBackend(remote)
|
||||
|
||||
b.CLI = remote.CLI
|
||||
b.ShowDiagnostics = remote.ShowDiagnostics
|
||||
|
||||
// Add a test provider to the local backend.
|
||||
p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"null_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Computed: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
p.ApplyResourceChangeResponse = providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("yes"),
|
||||
})}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// testServer returns a *httptest.Server used for local testing.
|
||||
func testServer(t *testing.T) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
|
@ -141,6 +171,49 @@ func testServer(t *testing.T) *httptest.Server {
|
|||
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
|
||||
})
|
||||
|
||||
// Respond to the initial query to read the organization settings.
|
||||
mux.HandleFunc("/api/v2/organizations/hashicorp", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/vnd.api+json")
|
||||
io.WriteString(w, `{
|
||||
"data": {
|
||||
"id": "hashicorp",
|
||||
"type": "organizations",
|
||||
"attributes": {
|
||||
"name": "hashicorp",
|
||||
"created-at": "2017-09-07T14:34:40.492Z",
|
||||
"email": "user@example.com",
|
||||
"collaborator-auth-policy": "password",
|
||||
"enterprise-plan": "premium",
|
||||
"permissions": {
|
||||
"can-update": true,
|
||||
"can-destroy": true,
|
||||
"can-create-team": true,
|
||||
"can-create-workspace": true,
|
||||
"can-update-oauth": true,
|
||||
"can-update-api-token": true,
|
||||
"can-update-sentinel": true,
|
||||
"can-traverse": true,
|
||||
"can-create-workspace-migration": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
})
|
||||
|
||||
// All tests that are assumed to pass will use the hashicorp organization,
|
||||
// so for all other organization requests we will return a 404.
|
||||
mux.HandleFunc("/api/v2/organizations/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
io.WriteString(w, `{
|
||||
"errors": [
|
||||
{
|
||||
"status": "404",
|
||||
"title": "not found"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -66,7 +66,7 @@ require (
|
|||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
||||
github.com/hashicorp/go-tfe v0.3.0
|
||||
github.com/hashicorp/go-tfe v0.3.1
|
||||
github.com/hashicorp/go-uuid v1.0.0
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -147,8 +147,8 @@ github.com/hashicorp/go-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aF
|
|||
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-tfe v0.3.0 h1:X0oM8RNKgMlmaMOEzLkx8/RTIC3d2K30R8+G4cSXJPc=
|
||||
github.com/hashicorp/go-tfe v0.3.0/go.mod h1:SRMjgjY06SfEKstIPRUVMtQfhSYR2H3GHVop0lfedkY=
|
||||
github.com/hashicorp/go-tfe v0.3.1 h1:178hBlqjBsXohfcJ2/t2RM8c29IviQrEkj+mqdbkQzM=
|
||||
github.com/hashicorp/go-tfe v0.3.1/go.mod h1:SRMjgjY06SfEKstIPRUVMtQfhSYR2H3GHVop0lfedkY=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
||||
|
|
|
@ -289,25 +289,6 @@ func (c *Client) configureLimiter() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ListOptions is used to specify pagination options when making API requests.
|
||||
// Pagination allows breaking up large result sets into chunks, or "pages".
|
||||
type ListOptions struct {
|
||||
// The page number to request. The results vary based on the PageSize.
|
||||
PageNumber int `url:"page[number],omitempty"`
|
||||
|
||||
// The number of elements returned in a single page.
|
||||
PageSize int `url:"page[size],omitempty"`
|
||||
}
|
||||
|
||||
// Pagination is used to return the pagination details of an API request.
|
||||
type Pagination struct {
|
||||
CurrentPage int `json:"current-page"`
|
||||
PreviousPage int `json:"prev-page"`
|
||||
NextPage int `json:"next-page"`
|
||||
TotalPages int `json:"total-pages"`
|
||||
TotalCount int `json:"total-count"`
|
||||
}
|
||||
|
||||
// newRequest creates an API request. A relative URL path can be provided in
|
||||
// path, in which case it is resolved relative to the apiVersionPath of the
|
||||
// Client. Relative URL paths should always be specified without a preceding
|
||||
|
@ -479,6 +460,25 @@ func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface
|
|||
return nil
|
||||
}
|
||||
|
||||
// ListOptions is used to specify pagination options when making API requests.
|
||||
// Pagination allows breaking up large result sets into chunks, or "pages".
|
||||
type ListOptions struct {
|
||||
// The page number to request. The results vary based on the PageSize.
|
||||
PageNumber int `url:"page[number],omitempty"`
|
||||
|
||||
// The number of elements returned in a single page.
|
||||
PageSize int `url:"page[size],omitempty"`
|
||||
}
|
||||
|
||||
// Pagination is used to return the pagination details of an API request.
|
||||
type Pagination struct {
|
||||
CurrentPage int `json:"current-page"`
|
||||
PreviousPage int `json:"prev-page"`
|
||||
NextPage int `json:"next-page"`
|
||||
TotalPages int `json:"total-pages"`
|
||||
TotalCount int `json:"total-count"`
|
||||
}
|
||||
|
||||
func parsePagination(body io.Reader) (*Pagination, error) {
|
||||
var raw struct {
|
||||
Meta struct {
|
||||
|
|
|
@ -66,6 +66,7 @@ type Workspace struct {
|
|||
Locked bool `jsonapi:"attr,locked"`
|
||||
MigrationEnvironment string `jsonapi:"attr,migration-environment"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Operations bool `jsonapi:"attr,operations"`
|
||||
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
|
||||
TerraformVersion string `jsonapi:"attr,terraform-version"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
|
|
|
@ -315,7 +315,7 @@ github.com/hashicorp/go-rootcerts
|
|||
github.com/hashicorp/go-safetemp
|
||||
# github.com/hashicorp/go-slug v0.1.0
|
||||
github.com/hashicorp/go-slug
|
||||
# github.com/hashicorp/go-tfe v0.3.0
|
||||
# github.com/hashicorp/go-tfe v0.3.1
|
||||
github.com/hashicorp/go-tfe
|
||||
# github.com/hashicorp/go-uuid v1.0.0
|
||||
github.com/hashicorp/go-uuid
|
||||
|
|
Loading…
Reference in New Issue