github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote/backend_common.go (about)

     1  package remote
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	tfe "github.com/hashicorp/go-tfe"
    15  	"github.com/hashicorp/terraform/backend"
    16  	"github.com/hashicorp/terraform/terraform"
    17  )
    18  
    19  var (
    20  	errApplyDiscarded   = errors.New("Apply discarded.")
    21  	errDestroyDiscarded = errors.New("Destroy discarded.")
    22  	errRunApproved      = errors.New("approved using the UI or API")
    23  	errRunDiscarded     = errors.New("discarded using the UI or API")
    24  	errRunOverridden    = errors.New("overridden using the UI or API")
    25  )
    26  
    27  // backoff will perform exponential backoff based on the iteration and
    28  // limited by the provided min and max (in milliseconds) durations.
    29  func backoff(min, max float64, iter int) time.Duration {
    30  	backoff := math.Pow(2, float64(iter)/5) * min
    31  	if backoff > max {
    32  		backoff = max
    33  	}
    34  	return time.Duration(backoff) * time.Millisecond
    35  }
    36  
    37  func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) {
    38  	started := time.Now()
    39  	updated := started
    40  	for i := 0; ; i++ {
    41  		select {
    42  		case <-stopCtx.Done():
    43  			return r, stopCtx.Err()
    44  		case <-cancelCtx.Done():
    45  			return r, cancelCtx.Err()
    46  		case <-time.After(backoff(1000, 3000, i)):
    47  			// Timer up, show status
    48  		}
    49  
    50  		// Retrieve the run to get its current status.
    51  		r, err := b.client.Runs.Read(stopCtx, r.ID)
    52  		if err != nil {
    53  			return r, generalError("Failed to retrieve run", err)
    54  		}
    55  
    56  		// Return if the run is no longer pending.
    57  		if r.Status != tfe.RunPending && r.Status != tfe.RunConfirmed {
    58  			if i == 0 && opType == "plan" && b.CLI != nil {
    59  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Waiting for the %s to start...\n", opType)))
    60  			}
    61  			if i > 0 && b.CLI != nil {
    62  				// Insert a blank line to separate the ouputs.
    63  				b.CLI.Output("")
    64  			}
    65  			return r, nil
    66  		}
    67  
    68  		// Check if 30 seconds have passed since the last update.
    69  		current := time.Now()
    70  		if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
    71  			updated = current
    72  			position := 0
    73  			elapsed := ""
    74  
    75  			// Calculate and set the elapsed time.
    76  			if i > 0 {
    77  				elapsed = fmt.Sprintf(
    78  					" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
    79  			}
    80  
    81  			// Retrieve the workspace used to run this operation in.
    82  			w, err = b.client.Workspaces.Read(stopCtx, b.organization, w.Name)
    83  			if err != nil {
    84  				return nil, generalError("Failed to retrieve workspace", err)
    85  			}
    86  
    87  			// If the workspace is locked the run will not be queued and we can
    88  			// update the status without making any expensive calls.
    89  			if w.Locked && w.CurrentRun != nil {
    90  				cr, err := b.client.Runs.Read(stopCtx, w.CurrentRun.ID)
    91  				if err != nil {
    92  					return r, generalError("Failed to retrieve current run", err)
    93  				}
    94  				if cr.Status == tfe.RunPending {
    95  					b.CLI.Output(b.Colorize().Color(
    96  						"Waiting for the manually locked workspace to be unlocked..." + elapsed))
    97  					continue
    98  				}
    99  			}
   100  
   101  			// Skip checking the workspace queue when we are the current run.
   102  			if w.CurrentRun == nil || w.CurrentRun.ID != r.ID {
   103  				found := false
   104  				options := tfe.RunListOptions{}
   105  			runlist:
   106  				for {
   107  					rl, err := b.client.Runs.List(stopCtx, w.ID, options)
   108  					if err != nil {
   109  						return r, generalError("Failed to retrieve run list", err)
   110  					}
   111  
   112  					// Loop through all runs to calculate the workspace queue position.
   113  					for _, item := range rl.Items {
   114  						if !found {
   115  							if r.ID == item.ID {
   116  								found = true
   117  							}
   118  							continue
   119  						}
   120  
   121  						// If the run is in a final state, ignore it and continue.
   122  						switch item.Status {
   123  						case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored:
   124  							continue
   125  						case tfe.RunPlanned:
   126  							if op.Type == backend.OperationTypePlan {
   127  								continue
   128  							}
   129  						}
   130  
   131  						// Increase the workspace queue position.
   132  						position++
   133  
   134  						// Stop searching when we reached the current run.
   135  						if w.CurrentRun != nil && w.CurrentRun.ID == item.ID {
   136  							break runlist
   137  						}
   138  					}
   139  
   140  					// Exit the loop when we've seen all pages.
   141  					if rl.CurrentPage >= rl.TotalPages {
   142  						break
   143  					}
   144  
   145  					// Update the page number to get the next page.
   146  					options.PageNumber = rl.NextPage
   147  				}
   148  
   149  				if position > 0 {
   150  					b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   151  						"Waiting for %d run(s) to finish before being queued...%s",
   152  						position,
   153  						elapsed,
   154  					)))
   155  					continue
   156  				}
   157  			}
   158  
   159  			options := tfe.RunQueueOptions{}
   160  		search:
   161  			for {
   162  				rq, err := b.client.Organizations.RunQueue(stopCtx, b.organization, options)
   163  				if err != nil {
   164  					return r, generalError("Failed to retrieve queue", err)
   165  				}
   166  
   167  				// Search through all queued items to find our run.
   168  				for _, item := range rq.Items {
   169  					if r.ID == item.ID {
   170  						position = item.PositionInQueue
   171  						break search
   172  					}
   173  				}
   174  
   175  				// Exit the loop when we've seen all pages.
   176  				if rq.CurrentPage >= rq.TotalPages {
   177  					break
   178  				}
   179  
   180  				// Update the page number to get the next page.
   181  				options.PageNumber = rq.NextPage
   182  			}
   183  
   184  			if position > 0 {
   185  				c, err := b.client.Organizations.Capacity(stopCtx, b.organization)
   186  				if err != nil {
   187  					return r, generalError("Failed to retrieve capacity", err)
   188  				}
   189  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   190  					"Waiting for %d queued run(s) to finish before starting...%s",
   191  					position-c.Running,
   192  					elapsed,
   193  				)))
   194  				continue
   195  			}
   196  
   197  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   198  				"Waiting for the %s to start...%s", opType, elapsed)))
   199  		}
   200  	}
   201  }
   202  
   203  // hasExplicitVariableValues is a best-effort check to determine whether the
   204  // user has provided -var or -var-file arguments to a remote operation.
   205  //
   206  // The results may be inaccurate if the configuration is invalid or if
   207  // individual variable values are invalid. That's okay because we only use this
   208  // result to hint the user to set variables a different way. It's always the
   209  // remote system's responsibility to do final validation of the input.
   210  func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool {
   211  	// Load the configuration using the caller-provided configuration loader.
   212  	config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
   213  	if configDiags.HasErrors() {
   214  		// If we can't load the configuration then we'll assume no explicit
   215  		// variable values just to let the remote operation start and let
   216  		// the remote system return the same set of configuration errors.
   217  		return false
   218  	}
   219  
   220  	// We're intentionally ignoring the diagnostics here because validation
   221  	// of the variable values is the responsibilty of the remote system. Our
   222  	// goal here is just to make a best effort count of how many variable
   223  	// values are coming from -var or -var-file CLI arguments so that we can
   224  	// hint the user that those are not supported for remote operations.
   225  	variables, _ := backend.ParseVariableValues(op.Variables, config.Module.Variables)
   226  
   227  	// Check for explicitly-defined (-var and -var-file) variables, which the
   228  	// remote backend does not support. All other source types are okay,
   229  	// because they are implicit from the execution context anyway and so
   230  	// their final values will come from the _remote_ execution context.
   231  	for _, v := range variables {
   232  		switch v.SourceType {
   233  		case terraform.ValueFromCLIArg, terraform.ValueFromNamedFile:
   234  			return true
   235  		}
   236  	}
   237  
   238  	return false
   239  }
   240  
   241  func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   242  	if r.CostEstimate == nil {
   243  		return nil
   244  	}
   245  
   246  	if b.CLI != nil {
   247  		b.CLI.Output("\n------------------------------------------------------------------------\n")
   248  	}
   249  
   250  	msgPrefix := "Cost estimation"
   251  	if b.CLI != nil {
   252  		b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
   253  	}
   254  
   255  	started := time.Now()
   256  	updated := started
   257  	for i := 0; ; i++ {
   258  		select {
   259  		case <-stopCtx.Done():
   260  			return stopCtx.Err()
   261  		case <-cancelCtx.Done():
   262  			return cancelCtx.Err()
   263  		case <-time.After(1 * time.Second):
   264  		}
   265  
   266  		// Retrieve the cost estimate to get its current status.
   267  		ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID)
   268  		if err != nil {
   269  			return generalError("Failed to retrieve cost estimate", err)
   270  		}
   271  
   272  		switch ce.Status {
   273  		case tfe.CostEstimateFinished:
   274  			delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64)
   275  			if err != nil {
   276  				return generalError("Unexpected error", err)
   277  			}
   278  
   279  			sign := "+"
   280  			if delta < 0 {
   281  				sign = "-"
   282  			}
   283  
   284  			deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
   285  
   286  			if b.CLI != nil {
   287  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
   288  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("           $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
   289  
   290  				if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply {
   291  					b.CLI.Output("\n------------------------------------------------------------------------")
   292  				}
   293  			}
   294  
   295  			return nil
   296  		case tfe.CostEstimatePending, tfe.CostEstimateQueued:
   297  			// Check if 30 seconds have passed since the last update.
   298  			current := time.Now()
   299  			if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
   300  				updated = current
   301  				elapsed := ""
   302  
   303  				// Calculate and set the elapsed time.
   304  				if i > 0 {
   305  					elapsed = fmt.Sprintf(
   306  						" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
   307  				}
   308  				b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n"))
   309  			}
   310  			continue
   311  		case tfe.CostEstimateSkippedDueToTargeting:
   312  			b.CLI.Output("Not available for this plan, because it was created with the -target option.")
   313  			b.CLI.Output("\n------------------------------------------------------------------------")
   314  			return nil
   315  		case tfe.CostEstimateErrored:
   316  			return fmt.Errorf(msgPrefix + " errored.")
   317  		case tfe.CostEstimateCanceled:
   318  			return fmt.Errorf(msgPrefix + " canceled.")
   319  		default:
   320  			return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status)
   321  		}
   322  	}
   323  	return nil
   324  }
   325  
   326  func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   327  	if b.CLI != nil {
   328  		b.CLI.Output("\n------------------------------------------------------------------------\n")
   329  	}
   330  	for i, pc := range r.PolicyChecks {
   331  		logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID)
   332  		if err != nil {
   333  			return generalError("Failed to retrieve policy check logs", err)
   334  		}
   335  		reader := bufio.NewReaderSize(logs, 64*1024)
   336  
   337  		// Retrieve the policy check to get its current status.
   338  		pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID)
   339  		if err != nil {
   340  			return generalError("Failed to retrieve policy check", err)
   341  		}
   342  
   343  		var msgPrefix string
   344  		switch pc.Scope {
   345  		case tfe.PolicyScopeOrganization:
   346  			msgPrefix = "Organization policy check"
   347  		case tfe.PolicyScopeWorkspace:
   348  			msgPrefix = "Workspace policy check"
   349  		default:
   350  			msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope)
   351  		}
   352  
   353  		if b.CLI != nil {
   354  			b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
   355  		}
   356  
   357  		if b.CLI != nil {
   358  			for next := true; next; {
   359  				var l, line []byte
   360  
   361  				for isPrefix := true; isPrefix; {
   362  					l, isPrefix, err = reader.ReadLine()
   363  					if err != nil {
   364  						if err != io.EOF {
   365  							return generalError("Failed to read logs", err)
   366  						}
   367  						next = false
   368  					}
   369  					line = append(line, l...)
   370  				}
   371  
   372  				if next || len(line) > 0 {
   373  					b.CLI.Output(b.Colorize().Color(string(line)))
   374  				}
   375  			}
   376  		}
   377  
   378  		switch pc.Status {
   379  		case tfe.PolicyPasses:
   380  			if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil {
   381  				b.CLI.Output("\n------------------------------------------------------------------------")
   382  			}
   383  			continue
   384  		case tfe.PolicyErrored:
   385  			return fmt.Errorf(msgPrefix + " errored.")
   386  		case tfe.PolicyHardFailed:
   387  			return fmt.Errorf(msgPrefix + " hard failed.")
   388  		case tfe.PolicySoftFailed:
   389  			if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil ||
   390  				op.AutoApprove || !pc.Actions.IsOverridable || !pc.Permissions.CanOverride {
   391  				return fmt.Errorf(msgPrefix + " soft failed.")
   392  			}
   393  		default:
   394  			return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status)
   395  		}
   396  
   397  		opts := &terraform.InputOpts{
   398  			Id:          "override",
   399  			Query:       "\nDo you want to override the soft failed policy check?",
   400  			Description: "Only 'override' will be accepted to override.",
   401  		}
   402  
   403  		err = b.confirm(stopCtx, op, opts, r, "override")
   404  		if err != nil && err != errRunOverridden {
   405  			return err
   406  		}
   407  
   408  		if err != errRunOverridden {
   409  			if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil {
   410  				return generalError("Failed to override policy check", err)
   411  			}
   412  		}
   413  
   414  		if b.CLI != nil {
   415  			b.CLI.Output("------------------------------------------------------------------------")
   416  		}
   417  	}
   418  
   419  	return nil
   420  }
   421  
   422  func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error {
   423  	doneCtx, cancel := context.WithCancel(stopCtx)
   424  	result := make(chan error, 2)
   425  
   426  	go func() {
   427  		// Make sure we cancel doneCtx before we return
   428  		// so the input command is also canceled.
   429  		defer cancel()
   430  
   431  		for {
   432  			select {
   433  			case <-doneCtx.Done():
   434  				return
   435  			case <-stopCtx.Done():
   436  				return
   437  			case <-time.After(3 * time.Second):
   438  				// Retrieve the run again to get its current status.
   439  				r, err := b.client.Runs.Read(stopCtx, r.ID)
   440  				if err != nil {
   441  					result <- generalError("Failed to retrieve run", err)
   442  					return
   443  				}
   444  
   445  				switch keyword {
   446  				case "override":
   447  					if r.Status != tfe.RunPolicyOverride {
   448  						if r.Status == tfe.RunDiscarded {
   449  							err = errRunDiscarded
   450  						} else {
   451  							err = errRunOverridden
   452  						}
   453  					}
   454  				case "yes":
   455  					if !r.Actions.IsConfirmable {
   456  						if r.Status == tfe.RunDiscarded {
   457  							err = errRunDiscarded
   458  						} else {
   459  							err = errRunApproved
   460  						}
   461  					}
   462  				}
   463  
   464  				if err != nil {
   465  					if b.CLI != nil {
   466  						b.CLI.Output(b.Colorize().Color(
   467  							fmt.Sprintf("[reset][yellow]%s[reset]", err.Error())))
   468  					}
   469  
   470  					if err == errRunDiscarded {
   471  						if op.Destroy {
   472  							err = errDestroyDiscarded
   473  						}
   474  						err = errApplyDiscarded
   475  					}
   476  
   477  					result <- err
   478  					return
   479  				}
   480  			}
   481  		}
   482  	}()
   483  
   484  	result <- func() error {
   485  		v, err := op.UIIn.Input(doneCtx, opts)
   486  		if err != nil && err != context.Canceled && stopCtx.Err() != context.Canceled {
   487  			return fmt.Errorf("Error asking %s: %v", opts.Id, err)
   488  		}
   489  
   490  		// We return the error of our parent channel as we don't
   491  		// care about the error of the doneCtx which is only used
   492  		// within this function. So if the doneCtx was canceled
   493  		// because stopCtx was canceled, this will properly return
   494  		// a context.Canceled error and otherwise it returns nil.
   495  		if doneCtx.Err() == context.Canceled || stopCtx.Err() == context.Canceled {
   496  			return stopCtx.Err()
   497  		}
   498  
   499  		// Make sure we cancel the context here so the loop that
   500  		// checks for external changes to the run is ended before
   501  		// we start to make changes ourselves.
   502  		cancel()
   503  
   504  		if v != keyword {
   505  			// Retrieve the run again to get its current status.
   506  			r, err = b.client.Runs.Read(stopCtx, r.ID)
   507  			if err != nil {
   508  				return generalError("Failed to retrieve run", err)
   509  			}
   510  
   511  			// Make sure we discard the run if possible.
   512  			if r.Actions.IsDiscardable {
   513  				err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
   514  				if err != nil {
   515  					if op.Destroy {
   516  						return generalError("Failed to discard destroy", err)
   517  					}
   518  					return generalError("Failed to discard apply", err)
   519  				}
   520  			}
   521  
   522  			// Even if the run was discarded successfully, we still
   523  			// return an error as the apply command was canceled.
   524  			if op.Destroy {
   525  				return errDestroyDiscarded
   526  			}
   527  			return errApplyDiscarded
   528  		}
   529  
   530  		return nil
   531  	}()
   532  
   533  	return <-result
   534  }