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