package communicator import ( "context" "fmt" "io" "log" "sync/atomic" "time" "github.com/hashicorp/terraform/internal/communicator/remote" "github.com/hashicorp/terraform/internal/communicator/shared" "github.com/hashicorp/terraform/internal/communicator/ssh" "github.com/hashicorp/terraform/internal/communicator/winrm" "github.com/hashicorp/terraform/internal/provisioners" "github.com/zclconf/go-cty/cty" ) // Communicator is an interface that must be implemented by all communicators // used for any of the provisioners type Communicator interface { // Connect is used to set up the connection Connect(provisioners.UIOutput) error // Disconnect is used to terminate the connection Disconnect() error // Timeout returns the configured connection timeout Timeout() time.Duration // ScriptPath returns the configured script path ScriptPath() string // Start executes a remote command in a new session Start(*remote.Cmd) error // Upload is used to upload a single file Upload(string, io.Reader) error // UploadScript is used to upload a file as an executable script UploadScript(string, io.Reader) error // UploadDir is used to upload a directory UploadDir(string, string) error } // New returns a configured Communicator or an error if the connection type is not supported func New(v cty.Value) (Communicator, error) { v, err := shared.ConnectionBlockSupersetSchema.CoerceValue(v) if err != nil { return nil, err } typeVal := v.GetAttr("type") connType := "" if !typeVal.IsNull() { connType = typeVal.AsString() } switch connType { case "ssh", "": // The default connection type is ssh, so if connType is empty use ssh return ssh.New(v) case "winrm": return winrm.New(v) default: return nil, fmt.Errorf("connection type '%s' not supported", connType) } } // maxBackoffDelay is the maximum delay between retry attempts var maxBackoffDelay = 20 * time.Second var initialBackoffDelay = time.Second // Fatal is an interface that error values can return to halt Retry type Fatal interface { FatalError() error } // Retry retries the function f until it returns a nil error, a Fatal error, or // the context expires. func Retry(ctx context.Context, f func() error) error { // container for atomic error value type errWrap struct { E error } // Try the function in a goroutine var errVal atomic.Value doneCh := make(chan struct{}) go func() { defer close(doneCh) delay := time.Duration(0) for { // If our context ended, we want to exit right away. select { case <-ctx.Done(): return case <-time.After(delay): } // Try the function call err := f() // return if we have no error, or a FatalError done := false switch e := err.(type) { case nil: done = true case Fatal: err = e.FatalError() done = true } errVal.Store(errWrap{err}) if done { return } log.Printf("[WARN] retryable error: %v", err) delay *= 2 if delay == 0 { delay = initialBackoffDelay } if delay > maxBackoffDelay { delay = maxBackoffDelay } log.Printf("[INFO] sleeping for %s", delay) } }() // Wait for completion select { case <-ctx.Done(): case <-doneCh: } var lastErr error // Check if we got an error executing if ev, ok := errVal.Load().(errWrap); ok { lastErr = ev.E } // Check if we have a context error to check if we're interrupted or timeout switch ctx.Err() { case context.Canceled: return fmt.Errorf("interrupted - last error: %v", lastErr) case context.DeadlineExceeded: return fmt.Errorf("timeout - last error: %v", lastErr) } if lastErr != nil { return lastErr } return nil }