core: add a context to the UIInput interface

This commit is contained in:
Sander van Harmelen 2019-03-07 21:07:13 +01:00
parent a95d97f066
commit 973e2a7cf9
22 changed files with 55 additions and 36 deletions

View File

@ -112,7 +112,7 @@ func (b *Local) opApply(
b.CLI.Output("") b.CLI.Output("")
} }
v, err := op.UIIn.Input(&terraform.InputOpts{ v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{
Id: "approve", Id: "approve",
Query: query, Query: query,
Description: desc, Description: desc,

View File

@ -15,7 +15,6 @@ import (
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/svchost" "github.com/hashicorp/terraform/svchost"
@ -69,9 +68,6 @@ type Remote struct {
// configuration. // configuration.
prefix string prefix string
// schema defines the configuration for the backend.
schema *schema.Backend
// services is used for service discovery // services is used for service discovery
services *disco.Disco services *disco.Disco
@ -726,13 +722,13 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
} }
func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
if r.Status == tfe.RunPending && r.Actions.IsCancelable { if r.Actions.IsCancelable {
// Only ask if the remote operation should be canceled // Only ask if the remote operation should be canceled
// if the auto approve flag is not set. // if the auto approve flag is not set.
if !op.AutoApprove { if !op.AutoApprove {
v, err := op.UIIn.Input(&terraform.InputOpts{ v, err := op.UIIn.Input(cancelCtx, &terraform.InputOpts{
Id: "cancel", Id: "cancel",
Query: "\nDo you want to cancel the pending remote operation?", Query: "\nDo you want to cancel the remote operation?",
Description: "Only 'yes' will be accepted to cancel.", Description: "Only 'yes' will be accepted to cancel.",
}) })
if err != nil { if err != nil {

View File

@ -313,7 +313,7 @@ func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Ope
} }
func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error {
v, err := op.UIIn.Input(opts) v, err := op.UIIn.Input(stopCtx, opts)
if err != nil { if err != nil {
return fmt.Errorf("Error asking %s: %v", opts.Id, err) return fmt.Errorf("Error asking %s: %v", opts.Id, err)
} }

View File

@ -217,7 +217,7 @@ type mockInput struct {
answers map[string]string answers map[string]string
} }
func (m *mockInput) Input(opts *terraform.InputOpts) (string, error) { func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
v, ok := m.answers[opts.Id] v, ok := m.answers[opts.Id]
if !ok { if !ok {
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -127,7 +128,7 @@ command and dealing with them before running this command again.
if dir != "." { if dir != "." {
query = fmt.Sprintf("Would you like to upgrade the module in %s?", dir) query = fmt.Sprintf("Would you like to upgrade the module in %s?", dir)
} }
v, err := c.UIInput().Input(&terraform.InputOpts{ v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "approve", Id: "approve",
Query: query, Query: query,
Description: `Only 'yes' will be accepted to confirm.`, Description: `Only 'yes' will be accepted to confirm.`,

View File

@ -128,7 +128,7 @@ type Meta struct {
// //
// stateOutPath is used to override the output path for the state. // stateOutPath is used to override the output path for the state.
// If not provided, the StatePath is used causing the old state to // If not provided, the StatePath is used causing the old state to
// be overriden. // be overridden.
// //
// backupPath is used to backup the state file before writing a modified // backupPath is used to backup the state file before writing a modified
// version. It defaults to stateOutPath + DefaultBackupExtension // version. It defaults to stateOutPath + DefaultBackupExtension
@ -470,7 +470,7 @@ func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
} }
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
v, err := m.UIInput().Input(opts) v, err := m.UIInput().Input(context.Background(), opts)
if err != nil { if err != nil {
return false, fmt.Errorf( return false, fmt.Errorf(
"Error asking for confirmation: %s", err) "Error asking for confirmation: %s", err)

View File

@ -773,7 +773,7 @@ func (m *Meta) selectWorkspace(b backend.Backend) error {
// If the selected workspace is not migrated, ask the user to select // If the selected workspace is not migrated, ask the user to select
// a workspace from the list of migrated workspaces. // a workspace from the list of migrated workspaces.
v, err := m.UIInput().Input(&terraform.InputOpts{ v, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "select-workspace", Id: "select-workspace",
Query: fmt.Sprintf( Query: fmt.Sprintf(
"\n[reset][bold][yellow]The currently selected workspace (%s) is not migrated.[reset]", "\n[reset][bold][yellow]The currently selected workspace (%s) is not migrated.[reset]",

View File

@ -242,7 +242,7 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
// for a new name and migrate the default state to the given named state. // for a new name and migrate the default state to the given named state.
stateTwo, err = func() (statemgr.Full, error) { stateTwo, err = func() (statemgr.Full, error) {
log.Print("[TRACE] backendMigrateState: target doesn't support a default workspace, so we must prompt for a new name") log.Print("[TRACE] backendMigrateState: target doesn't support a default workspace, so we must prompt for a new name")
name, err := m.UIInput().Input(&terraform.InputOpts{ name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "new-state-name", Id: "new-state-name",
Query: fmt.Sprintf( Query: fmt.Sprintf(
"[reset][bold][yellow]The %q backend configuration only allows "+ "[reset][bold][yellow]The %q backend configuration only allows "+

View File

@ -1,8 +1,8 @@
package command package command
import ( import (
"context"
"fmt" "fmt"
"github.com/hashicorp/terraform/internal/earlyconfig"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -13,6 +13,7 @@ import (
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/earlyconfig"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/registry" "github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -285,7 +286,7 @@ func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.
attrS := schema.Attributes[name] attrS := schema.Attributes[name]
for { for {
strVal, err := input.Input(&terraform.InputOpts{ strVal, err := input.Input(context.Background(), &terraform.InputOpts{
Id: name, Id: name,
Query: name, Query: name,
Description: attrS.Description, Description: attrS.Description,

View File

@ -3,6 +3,7 @@ package command
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -38,7 +39,7 @@ type UIInput struct {
once sync.Once once sync.Once
} }
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) { func (i *UIInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
i.once.Do(i.init) i.once.Do(i.init)
r := i.Reader r := i.Reader
@ -137,6 +138,12 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
} }
return line, nil return line, nil
case <-ctx.Done():
// Print a newline so that any further output starts properly
// on a new line.
fmt.Fprintln(w)
return "", ctx.Err()
case <-sigCh: case <-sigCh:
// Print a newline so that any further output starts properly // Print a newline so that any further output starts properly
// on a new line. // on a new line.

View File

@ -2,6 +2,7 @@ package command
import ( import (
"bytes" "bytes"
"context"
"testing" "testing"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -17,7 +18,7 @@ func TestUIInputInput(t *testing.T) {
Writer: bytes.NewBuffer(nil), Writer: bytes.NewBuffer(nil),
} }
v, err := i.Input(&terraform.InputOpts{}) v, err := i.Input(context.Background(), &terraform.InputOpts{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -33,7 +34,7 @@ func TestUIInputInput_spaces(t *testing.T) {
Writer: bytes.NewBuffer(nil), Writer: bytes.NewBuffer(nil),
} }
v, err := i.Input(&terraform.InputOpts{}) v, err := i.Input(context.Background(), &terraform.InputOpts{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@ -88,7 +89,7 @@ func (c *UnlockCommand) Run(args []string) int {
"This will allow local Terraform commands to modify this state, even though it\n" + "This will allow local Terraform commands to modify this state, even though it\n" +
"may be still be in use. Only 'yes' will be accepted to confirm." "may be still be in use. Only 'yes' will be accepted to confirm."
v, err := c.UIInput().Input(&terraform.InputOpts{ v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "force-unlock", Id: "force-unlock",
Query: "Do you really want to force-unlock?", Query: "Do you really want to force-unlock?",
Description: desc, Description: desc,

View File

@ -12,6 +12,7 @@
package schema package schema
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
@ -1255,7 +1256,7 @@ func (m schemaMap) inputString(
input terraform.UIInput, input terraform.UIInput,
k string, k string,
schema *Schema) (interface{}, error) { schema *Schema) (interface{}, error) {
result, err := input.Input(&terraform.InputOpts{ result, err := input.Input(context.Background(), &terraform.InputOpts{
Id: k, Id: k,
Query: k, Query: k,
Description: schema.Description, Description: schema.Description,

View File

@ -170,12 +170,12 @@ func TestVersionListing(t *testing.T) {
} }
if len(versions) != len(expected) { if len(versions) != len(expected) {
t.Fatalf("Received wrong number of versions. expected: %q, got: %q", expected, versions) t.Fatalf("Received wrong number of versions. expected: %#v, got: %#v", expected, versions)
} }
for i, v := range versions { for i, v := range versions {
if v.Version != expected[i].Version { if v.Version != expected[i].Version {
t.Fatalf("incorrect version: %q, expected %q", v, expected[i]) t.Fatalf("incorrect version: %#v, expected %#v", v, expected[i])
} }
} }
} }

View File

@ -1,19 +1,20 @@
package plugin package plugin
import ( import (
"context"
"net/rpc" "net/rpc"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// UIInput is an implementatin of terraform.UIInput that communicates // UIInput is an implementation of terraform.UIInput that communicates
// over RPC. // over RPC.
type UIInput struct { type UIInput struct {
Client *rpc.Client Client *rpc.Client
} }
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) { func (i *UIInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
var resp UIInputInputResponse var resp UIInputInputResponse
err := i.Client.Call("Plugin.Input", opts, &resp) err := i.Client.Call("Plugin.Input", opts, &resp)
if err != nil { if err != nil {
@ -41,7 +42,7 @@ type UIInputServer struct {
func (s *UIInputServer) Input( func (s *UIInputServer) Input(
opts *terraform.InputOpts, opts *terraform.InputOpts,
reply *UIInputInputResponse) error { reply *UIInputInputResponse) error {
value, err := s.UIInput.Input(opts) value, err := s.UIInput.Input(context.Background(), opts)
*reply = UIInputInputResponse{ *reply = UIInputInputResponse{
Value: value, Value: value,
Error: plugin.NewBasicError(err), Error: plugin.NewBasicError(err),

View File

@ -1,6 +1,7 @@
package plugin package plugin
import ( import (
"context"
"reflect" "reflect"
"testing" "testing"
@ -32,7 +33,7 @@ func TestUIInput_input(t *testing.T) {
Id: "foo", Id: "foo",
} }
v, err := input.Input(opts) v, err := input.Input(context.Background(), opts)
if !i.InputCalled { if !i.InputCalled {
t.Fatal("input should be called") t.Fatal("input should be called")
} }

View File

@ -232,7 +232,7 @@ func TestLookupProviderVersions(t *testing.T) {
for _, v := range resp.Versions { for _, v := range resp.Versions {
_, err := version.NewVersion(v.Version) _, err := version.NewVersion(v.Version)
if err != nil { if err != nil {
t.Fatalf("invalid version %q: %s", v, err) t.Fatalf("invalid version %#v: %v", v, err)
} }
} }
} }

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"sort" "sort"
@ -26,6 +27,8 @@ func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
return diags return diags
} }
ctx := context.Background()
if mode&InputModeVar != 0 { if mode&InputModeVar != 0 {
log.Printf("[TRACE] Context.Input: Prompting for variables") log.Printf("[TRACE] Context.Input: Prompting for variables")
@ -60,7 +63,7 @@ func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
retry := 0 retry := 0
for { for {
var err error var err error
rawValue, err = c.uiInput.Input(&InputOpts{ rawValue, err = c.uiInput.Input(ctx, &InputOpts{
Id: fmt.Sprintf("var.%s", n), Id: fmt.Sprintf("var.%s", n),
Query: fmt.Sprintf("var.%s", n), Query: fmt.Sprintf("var.%s", n),
Description: v.Description, Description: v.Description,
@ -208,7 +211,7 @@ func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
} }
log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key) log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key)
rawVal, err := input.Input(&InputOpts{ rawVal, err := input.Input(ctx, &InputOpts{
Id: key, Id: key,
Query: key, Query: key,
Description: attrS.Description, Description: attrS.Description,

View File

@ -1,10 +1,12 @@
package terraform package terraform
import "context"
// UIInput is the interface that must be implemented to ask for input // UIInput is the interface that must be implemented to ask for input
// from this user. This should forward the request to wherever the user // from this user. This should forward the request to wherever the user
// inputs things to ask for values. // inputs things to ask for values.
type UIInput interface { type UIInput interface {
Input(*InputOpts) (string, error) Input(context.Context, *InputOpts) (string, error)
} }
// InputOpts are options for asking for input. // InputOpts are options for asking for input.

View File

@ -1,5 +1,7 @@
package terraform package terraform
import "context"
// MockUIInput is an implementation of UIInput that can be used for tests. // MockUIInput is an implementation of UIInput that can be used for tests.
type MockUIInput struct { type MockUIInput struct {
InputCalled bool InputCalled bool
@ -10,7 +12,7 @@ type MockUIInput struct {
InputFn func(*InputOpts) (string, error) InputFn func(*InputOpts) (string, error)
} }
func (i *MockUIInput) Input(opts *InputOpts) (string, error) { func (i *MockUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) {
i.InputCalled = true i.InputCalled = true
i.InputOpts = opts i.InputOpts = opts
if i.InputFn != nil { if i.InputFn != nil {

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"fmt" "fmt"
) )
@ -12,8 +13,8 @@ type PrefixUIInput struct {
UIInput UIInput UIInput UIInput
} }
func (i *PrefixUIInput) Input(opts *InputOpts) (string, error) { func (i *PrefixUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) {
opts.Id = fmt.Sprintf("%s.%s", i.IdPrefix, opts.Id) opts.Id = fmt.Sprintf("%s.%s", i.IdPrefix, opts.Id)
opts.Query = fmt.Sprintf("%s%s", i.QueryPrefix, opts.Query) opts.Query = fmt.Sprintf("%s%s", i.QueryPrefix, opts.Query)
return i.UIInput.Input(opts) return i.UIInput.Input(ctx, opts)
} }

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"testing" "testing"
) )
@ -15,7 +16,7 @@ func testPrefixUIInput(t *testing.T) {
UIInput: input, UIInput: input,
} }
_, err := prefix.Input(&InputOpts{Id: "bar"}) _, err := prefix.Input(context.Background(), &InputOpts{Id: "bar"})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }