diff --git a/backend/init/init.go b/backend/init/init.go index 0e4f7188b..1ee473401 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -3,7 +3,6 @@ package init import ( - "os" "sync" "github.com/hashicorp/terraform/backend" @@ -48,14 +47,8 @@ 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 - }, + "local": func() backend.Backend { return backendLocal.New() }, + "remote": func() backend.Backend { return backendRemote.New(services) }, // Remote State backends. "artifactory": func() backend.Backend { return backendArtifactory.New() }, diff --git a/backend/init/init_test.go b/backend/init/init_test.go index 02eacb638..59653125a 100644 --- a/backend/init/init_test.go +++ b/backend/init/init_test.go @@ -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) - } - } -} diff --git a/backend/local/backend.go b/backend/local/backend.go index 5de7e1818..eb2053b23 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -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) diff --git a/backend/local/backend_apply_test.go b/backend/local/backend_apply_test.go index f9664b8ea..940d08bb0 100644 --- a/backend/local/backend_apply_test.go +++ b/backend/local/backend_apply_test.go @@ -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{ diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 950d83b77..ac1a7701b 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -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]++ diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 47a2f1a2e..1a48b39c3 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -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 diff --git a/backend/remote/backend_apply.go b/backend/remote/backend_apply.go index ae6d1eeac..4d78a0437 100644 --- a/backend/remote/backend_apply.go +++ b/backend/remote/backend_apply.go @@ -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 { diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index be7345e60..f68dea0cc 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -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) } } diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index eac6b6839..c65de0258 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -905,8 +905,9 @@ func (m *mockWorkspaces) List(ctx context.Context, organization string, options func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { w := &tfe.Workspace{ - ID: generateID("ws-"), - Name: *options.Name, + ID: generateID("ws-"), + Name: *options.Name, + Operations: !strings.HasSuffix(*options.Name, "no-operations"), Permissions: &tfe.WorkspacePermissions{ CanQueueRun: true, CanUpdate: true, diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go index 2fdea7781..8c48d83c6 100644 --- a/backend/remote/backend_plan.go +++ b/backend/remote/backend_plan.go @@ -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 { diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go index 6ce609d43..60c052d50 100644 --- a/backend/remote/backend_plan_test.go +++ b/backend/remote/backend_plan_test.go @@ -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) } } diff --git a/backend/remote/backend_test.go b/backend/remote/backend_test.go index 6e348d397..db476a231 100644 --- a/backend/remote/backend_test.go +++ b/backend/remote/backend_test.go @@ -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) } } diff --git a/backend/remote/cli.go b/backend/remote/cli.go index a6aa1103f..5a6afa7ec 100644 --- a/backend/remote/cli.go +++ b/backend/remote/cli.go @@ -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 } diff --git a/backend/remote/remote_test.go b/backend/remote/remote_test.go new file mode 100644 index 000000000..7fc332e49 --- /dev/null +++ b/backend/remote/remote_test.go @@ -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()) +} diff --git a/backend/remote/test-fixtures/apply-destroy/apply.log b/backend/remote/test-fixtures/apply-destroy/apply.log index 34adfcd6b..d126547d9 100644 --- a/backend/remote/test-fixtures/apply-destroy/apply.log +++ b/backend/remote/test-fixtures/apply-destroy/apply.log @@ -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 diff --git a/backend/remote/test-fixtures/apply-policy-passed/apply.log b/backend/remote/test-fixtures/apply-policy-passed/apply.log index 89c0dbc42..901994838 100644 --- a/backend/remote/test-fixtures/apply-policy-passed/apply.log +++ b/backend/remote/test-fixtures/apply-policy-passed/apply.log @@ -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) diff --git a/backend/remote/test-fixtures/apply-policy-soft-failed/apply.log b/backend/remote/test-fixtures/apply-policy-soft-failed/apply.log index 89c0dbc42..901994838 100644 --- a/backend/remote/test-fixtures/apply-policy-soft-failed/apply.log +++ b/backend/remote/test-fixtures/apply-policy-soft-failed/apply.log @@ -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) diff --git a/backend/remote/test-fixtures/apply-variables/apply.log b/backend/remote/test-fixtures/apply-variables/apply.log index 89c0dbc42..901994838 100644 --- a/backend/remote/test-fixtures/apply-variables/apply.log +++ b/backend/remote/test-fixtures/apply-variables/apply.log @@ -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) diff --git a/backend/remote/test-fixtures/apply/apply.log b/backend/remote/test-fixtures/apply/apply.log index 89c0dbc42..901994838 100644 --- a/backend/remote/test-fixtures/apply/apply.log +++ b/backend/remote/test-fixtures/apply/apply.log @@ -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) diff --git a/backend/remote/testing.go b/backend/remote/testing.go index 0bb8d66c9..54083dc23 100644 --- a/backend/remote/testing.go +++ b/backend/remote/testing.go @@ -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) } diff --git a/go.mod b/go.mod index 6332228c4..6a471e152 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 31194a3f7..43301678d 100644 --- a/go.sum +++ b/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= diff --git a/vendor/github.com/hashicorp/go-tfe/tfe.go b/vendor/github.com/hashicorp/go-tfe/tfe.go index c38938564..f2e83169d 100644 --- a/vendor/github.com/hashicorp/go-tfe/tfe.go +++ b/vendor/github.com/hashicorp/go-tfe/tfe.go @@ -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 { diff --git a/vendor/github.com/hashicorp/go-tfe/workspace.go b/vendor/github.com/hashicorp/go-tfe/workspace.go index ca3b21d06..968af91b7 100644 --- a/vendor/github.com/hashicorp/go-tfe/workspace.go +++ b/vendor/github.com/hashicorp/go-tfe/workspace.go @@ -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"` diff --git a/vendor/modules.txt b/vendor/modules.txt index 8073c8a4b..9f29433ce 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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