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