github.com/pulumi/terraform@v1.4.0/pkg/cloud/backend_common.go (about)

     1  package cloud
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"math"
    11  	"net/http"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/hashicorp/go-retryablehttp"
    18  	tfe "github.com/hashicorp/go-tfe"
    19  	"github.com/hashicorp/jsonapi"
    20  	"github.com/pulumi/terraform/pkg/backend"
    21  	"github.com/pulumi/terraform/pkg/command/jsonformat"
    22  	"github.com/pulumi/terraform/pkg/logging"
    23  	"github.com/pulumi/terraform/pkg/plans"
    24  	"github.com/pulumi/terraform/pkg/terraform"
    25  )
    26  
    27  var (
    28  	backoffMin = 1000.0
    29  	backoffMax = 3000.0
    30  
    31  	runPollInterval = 3 * time.Second
    32  )
    33  
    34  // backoff will perform exponential backoff based on the iteration and
    35  // limited by the provided min and max (in milliseconds) durations.
    36  func backoff(min, max float64, iter int) time.Duration {
    37  	backoff := math.Pow(2, float64(iter)/5) * min
    38  	if backoff > max {
    39  		backoff = max
    40  	}
    41  	return time.Duration(backoff) * time.Millisecond
    42  }
    43  
    44  func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) {
    45  	started := time.Now()
    46  	updated := started
    47  	for i := 0; ; i++ {
    48  		select {
    49  		case <-stopCtx.Done():
    50  			return r, stopCtx.Err()
    51  		case <-cancelCtx.Done():
    52  			return r, cancelCtx.Err()
    53  		case <-time.After(backoff(backoffMin, backoffMax, i)):
    54  			// Timer up, show status
    55  		}
    56  
    57  		// Retrieve the run to get its current status.
    58  		r, err := b.client.Runs.Read(stopCtx, r.ID)
    59  		if err != nil {
    60  			return r, generalError("Failed to retrieve run", err)
    61  		}
    62  
    63  		// Return if the run is no longer pending.
    64  		if r.Status != tfe.RunPending && r.Status != tfe.RunConfirmed {
    65  			if i == 0 && opType == "plan" && b.CLI != nil {
    66  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Waiting for the %s to start...\n", opType)))
    67  			}
    68  			if i > 0 && b.CLI != nil {
    69  				// Insert a blank line to separate the ouputs.
    70  				b.CLI.Output("")
    71  			}
    72  			return r, nil
    73  		}
    74  
    75  		// Check if 30 seconds have passed since the last update.
    76  		current := time.Now()
    77  		if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
    78  			updated = current
    79  			position := 0
    80  			elapsed := ""
    81  
    82  			// Calculate and set the elapsed time.
    83  			if i > 0 {
    84  				elapsed = fmt.Sprintf(
    85  					" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
    86  			}
    87  
    88  			// Retrieve the workspace used to run this operation in.
    89  			w, err = b.client.Workspaces.Read(stopCtx, b.organization, w.Name)
    90  			if err != nil {
    91  				return nil, generalError("Failed to retrieve workspace", err)
    92  			}
    93  
    94  			// If the workspace is locked the run will not be queued and we can
    95  			// update the status without making any expensive calls.
    96  			if w.Locked && w.CurrentRun != nil {
    97  				cr, err := b.client.Runs.Read(stopCtx, w.CurrentRun.ID)
    98  				if err != nil {
    99  					return r, generalError("Failed to retrieve current run", err)
   100  				}
   101  				if cr.Status == tfe.RunPending {
   102  					b.CLI.Output(b.Colorize().Color(
   103  						"Waiting for the manually locked workspace to be unlocked..." + elapsed))
   104  					continue
   105  				}
   106  			}
   107  
   108  			// Skip checking the workspace queue when we are the current run.
   109  			if w.CurrentRun == nil || w.CurrentRun.ID != r.ID {
   110  				found := false
   111  				options := &tfe.RunListOptions{}
   112  			runlist:
   113  				for {
   114  					rl, err := b.client.Runs.List(stopCtx, w.ID, options)
   115  					if err != nil {
   116  						return r, generalError("Failed to retrieve run list", err)
   117  					}
   118  
   119  					// Loop through all runs to calculate the workspace queue position.
   120  					for _, item := range rl.Items {
   121  						if !found {
   122  							if r.ID == item.ID {
   123  								found = true
   124  							}
   125  							continue
   126  						}
   127  
   128  						// If the run is in a final state, ignore it and continue.
   129  						switch item.Status {
   130  						case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored:
   131  							continue
   132  						case tfe.RunPlanned:
   133  							if op.Type == backend.OperationTypePlan {
   134  								continue
   135  							}
   136  						}
   137  
   138  						// Increase the workspace queue position.
   139  						position++
   140  
   141  						// Stop searching when we reached the current run.
   142  						if w.CurrentRun != nil && w.CurrentRun.ID == item.ID {
   143  							break runlist
   144  						}
   145  					}
   146  
   147  					// Exit the loop when we've seen all pages.
   148  					if rl.CurrentPage >= rl.TotalPages {
   149  						break
   150  					}
   151  
   152  					// Update the page number to get the next page.
   153  					options.PageNumber = rl.NextPage
   154  				}
   155  
   156  				if position > 0 {
   157  					b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   158  						"Waiting for %d run(s) to finish before being queued...%s",
   159  						position,
   160  						elapsed,
   161  					)))
   162  					continue
   163  				}
   164  			}
   165  
   166  			options := tfe.ReadRunQueueOptions{}
   167  		search:
   168  			for {
   169  				rq, err := b.client.Organizations.ReadRunQueue(stopCtx, b.organization, options)
   170  				if err != nil {
   171  					return r, generalError("Failed to retrieve queue", err)
   172  				}
   173  
   174  				// Search through all queued items to find our run.
   175  				for _, item := range rq.Items {
   176  					if r.ID == item.ID {
   177  						position = item.PositionInQueue
   178  						break search
   179  					}
   180  				}
   181  
   182  				// Exit the loop when we've seen all pages.
   183  				if rq.CurrentPage >= rq.TotalPages {
   184  					break
   185  				}
   186  
   187  				// Update the page number to get the next page.
   188  				options.PageNumber = rq.NextPage
   189  			}
   190  
   191  			if position > 0 {
   192  				c, err := b.client.Organizations.ReadCapacity(stopCtx, b.organization)
   193  				if err != nil {
   194  					return r, generalError("Failed to retrieve capacity", err)
   195  				}
   196  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   197  					"Waiting for %d queued run(s) to finish before starting...%s",
   198  					position-c.Running,
   199  					elapsed,
   200  				)))
   201  				continue
   202  			}
   203  
   204  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   205  				"Waiting for the %s to start...%s", opType, elapsed)))
   206  		}
   207  	}
   208  }
   209  
   210  func (b *Cloud) waitTaskStage(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run, stageID string, outputTitle string) error {
   211  	integration := &IntegrationContext{
   212  		B:             b,
   213  		StopContext:   stopCtx,
   214  		CancelContext: cancelCtx,
   215  		Op:            op,
   216  		Run:           r,
   217  	}
   218  	return b.runTaskStage(integration, integration.BeginOutput(outputTitle), stageID)
   219  }
   220  
   221  func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   222  	if r.CostEstimate == nil {
   223  		return nil
   224  	}
   225  
   226  	msgPrefix := "Cost Estimation"
   227  	started := time.Now()
   228  	updated := started
   229  	for i := 0; ; i++ {
   230  		select {
   231  		case <-stopCtx.Done():
   232  			return stopCtx.Err()
   233  		case <-cancelCtx.Done():
   234  			return cancelCtx.Err()
   235  		case <-time.After(backoff(backoffMin, backoffMax, i)):
   236  		}
   237  
   238  		// Retrieve the cost estimate to get its current status.
   239  		ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID)
   240  		if err != nil {
   241  			return generalError("Failed to retrieve cost estimate", err)
   242  		}
   243  
   244  		// If the run is canceled or errored, but the cost-estimate still has
   245  		// no result, there is nothing further to render.
   246  		if ce.Status != tfe.CostEstimateFinished {
   247  			if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored {
   248  				return nil
   249  			}
   250  		}
   251  
   252  		// checking if i == 0 so as to avoid printing this starting horizontal-rule
   253  		// every retry, and that it only prints it on the first (i=0) attempt.
   254  		if b.CLI != nil && i == 0 {
   255  			b.CLI.Output("\n------------------------------------------------------------------------\n")
   256  		}
   257  
   258  		switch ce.Status {
   259  		case tfe.CostEstimateFinished:
   260  			delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64)
   261  			if err != nil {
   262  				return generalError("Unexpected error", err)
   263  			}
   264  
   265  			sign := "+"
   266  			if delta < 0 {
   267  				sign = "-"
   268  			}
   269  
   270  			deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
   271  
   272  			if b.CLI != nil {
   273  				b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n"))
   274  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
   275  				b.CLI.Output(b.Colorize().Color(fmt.Sprintf("           $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
   276  
   277  				if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply {
   278  					b.CLI.Output("\n------------------------------------------------------------------------")
   279  				}
   280  			}
   281  
   282  			return nil
   283  		case tfe.CostEstimatePending, tfe.CostEstimateQueued:
   284  			// Check if 30 seconds have passed since the last update.
   285  			current := time.Now()
   286  			if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
   287  				updated = current
   288  				elapsed := ""
   289  
   290  				// Calculate and set the elapsed time.
   291  				if i > 0 {
   292  					elapsed = fmt.Sprintf(
   293  						" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
   294  				}
   295  				b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n"))
   296  				b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n"))
   297  			}
   298  			continue
   299  		case tfe.CostEstimateSkippedDueToTargeting:
   300  			b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n"))
   301  			b.CLI.Output("Not available for this plan, because it was created with the -target option.")
   302  			b.CLI.Output("\n------------------------------------------------------------------------")
   303  			return nil
   304  		case tfe.CostEstimateErrored:
   305  			b.CLI.Output(msgPrefix + " errored.\n")
   306  			b.CLI.Output("\n------------------------------------------------------------------------")
   307  			return nil
   308  		case tfe.CostEstimateCanceled:
   309  			return fmt.Errorf(msgPrefix + " canceled.")
   310  		default:
   311  			return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status)
   312  		}
   313  	}
   314  }
   315  
   316  func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   317  	if b.CLI != nil {
   318  		b.CLI.Output("\n------------------------------------------------------------------------\n")
   319  	}
   320  	for i, pc := range r.PolicyChecks {
   321  		// Read the policy check logs. This is a blocking call that will only
   322  		// return once the policy check is complete.
   323  		logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID)
   324  		if err != nil {
   325  			return generalError("Failed to retrieve policy check logs", err)
   326  		}
   327  		reader := bufio.NewReaderSize(logs, 64*1024)
   328  
   329  		// Retrieve the policy check to get its current status.
   330  		pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID)
   331  		if err != nil {
   332  			return generalError("Failed to retrieve policy check", err)
   333  		}
   334  
   335  		// If the run is canceled or errored, but the policy check still has
   336  		// no result, there is nothing further to render.
   337  		if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored {
   338  			switch pc.Status {
   339  			case tfe.PolicyPending, tfe.PolicyQueued, tfe.PolicyUnreachable:
   340  				continue
   341  			}
   342  		}
   343  
   344  		var msgPrefix string
   345  		switch pc.Scope {
   346  		case tfe.PolicyScopeOrganization:
   347  			msgPrefix = "Organization Policy Check"
   348  		case tfe.PolicyScopeWorkspace:
   349  			msgPrefix = "Workspace Policy Check"
   350  		default:
   351  			msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope)
   352  		}
   353  
   354  		if b.CLI != nil {
   355  			b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n"))
   356  		}
   357  
   358  		if b.CLI != nil {
   359  			for next := true; next; {
   360  				var l, line []byte
   361  
   362  				for isPrefix := true; isPrefix; {
   363  					l, isPrefix, err = reader.ReadLine()
   364  					if err != nil {
   365  						if err != io.EOF {
   366  							return generalError("Failed to read logs", err)
   367  						}
   368  						next = false
   369  					}
   370  					line = append(line, l...)
   371  				}
   372  
   373  				if next || len(line) > 0 {
   374  					b.CLI.Output(b.Colorize().Color(string(line)))
   375  				}
   376  			}
   377  		}
   378  
   379  		switch pc.Status {
   380  		case tfe.PolicyPasses:
   381  			if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil {
   382  				b.CLI.Output("\n------------------------------------------------------------------------")
   383  			}
   384  			continue
   385  		case tfe.PolicyErrored:
   386  			return fmt.Errorf(msgPrefix + " errored.")
   387  		case tfe.PolicyHardFailed:
   388  			return fmt.Errorf(msgPrefix + " hard failed.")
   389  		case tfe.PolicySoftFailed:
   390  			runUrl := fmt.Sprintf(runHeader, b.hostname, b.organization, op.Workspace, r.ID)
   391  
   392  			if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil ||
   393  				!pc.Actions.IsOverridable || !pc.Permissions.CanOverride {
   394  				return fmt.Errorf(msgPrefix + " soft failed.\n" + runUrl)
   395  			}
   396  
   397  			if op.AutoApprove {
   398  				if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil {
   399  					return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err)
   400  				}
   401  			} else if !b.input {
   402  				return errPolicyOverrideNeedsUIConfirmation
   403  			} else {
   404  				opts := &terraform.InputOpts{
   405  					Id:          "override",
   406  					Query:       "\nDo you want to override the soft failed policy check?",
   407  					Description: "Only 'override' will be accepted to override.",
   408  				}
   409  				err = b.confirm(stopCtx, op, opts, r, "override")
   410  				if err != nil && err != errRunOverridden {
   411  					return fmt.Errorf(
   412  						fmt.Sprintf("Failed to override: %s\n%s\n", err.Error(), runUrl),
   413  					)
   414  				}
   415  
   416  				if err != errRunOverridden {
   417  					if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil {
   418  						return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err)
   419  					}
   420  				} else {
   421  					b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runUrl))
   422  				}
   423  			}
   424  
   425  			if b.CLI != nil {
   426  				b.CLI.Output("------------------------------------------------------------------------")
   427  			}
   428  		default:
   429  			return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status)
   430  		}
   431  	}
   432  
   433  	return nil
   434  }
   435  
   436  func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error {
   437  	doneCtx, cancel := context.WithCancel(stopCtx)
   438  	result := make(chan error, 2)
   439  
   440  	go func() {
   441  		// Make sure we cancel doneCtx before we return
   442  		// so the input command is also canceled.
   443  		defer cancel()
   444  
   445  		for {
   446  			select {
   447  			case <-doneCtx.Done():
   448  				return
   449  			case <-stopCtx.Done():
   450  				return
   451  			case <-time.After(runPollInterval):
   452  				// Retrieve the run again to get its current status.
   453  				r, err := b.client.Runs.Read(stopCtx, r.ID)
   454  				if err != nil {
   455  					result <- generalError("Failed to retrieve run", err)
   456  					return
   457  				}
   458  
   459  				switch keyword {
   460  				case "override":
   461  					if r.Status != tfe.RunPolicyOverride && r.Status != tfe.RunPostPlanAwaitingDecision {
   462  						if r.Status == tfe.RunDiscarded {
   463  							err = errRunDiscarded
   464  						} else {
   465  							err = errRunOverridden
   466  						}
   467  					}
   468  				case "yes":
   469  					if !r.Actions.IsConfirmable {
   470  						if r.Status == tfe.RunDiscarded {
   471  							err = errRunDiscarded
   472  						} else {
   473  							err = errRunApproved
   474  						}
   475  					}
   476  				}
   477  
   478  				if err != nil {
   479  					if b.CLI != nil {
   480  						b.CLI.Output(b.Colorize().Color(
   481  							fmt.Sprintf("[reset][yellow]%s[reset]", err.Error())))
   482  					}
   483  
   484  					if err == errRunDiscarded {
   485  						err = errApplyDiscarded
   486  						if op.PlanMode == plans.DestroyMode {
   487  							err = errDestroyDiscarded
   488  						}
   489  					}
   490  
   491  					result <- err
   492  					return
   493  				}
   494  			}
   495  		}
   496  	}()
   497  
   498  	result <- func() error {
   499  		v, err := op.UIIn.Input(doneCtx, opts)
   500  		if err != nil && err != context.Canceled && stopCtx.Err() != context.Canceled {
   501  			return fmt.Errorf("Error asking %s: %v", opts.Id, err)
   502  		}
   503  
   504  		// We return the error of our parent channel as we don't
   505  		// care about the error of the doneCtx which is only used
   506  		// within this function. So if the doneCtx was canceled
   507  		// because stopCtx was canceled, this will properly return
   508  		// a context.Canceled error and otherwise it returns nil.
   509  		if doneCtx.Err() == context.Canceled || stopCtx.Err() == context.Canceled {
   510  			return stopCtx.Err()
   511  		}
   512  
   513  		// Make sure we cancel the context here so the loop that
   514  		// checks for external changes to the run is ended before
   515  		// we start to make changes ourselves.
   516  		cancel()
   517  
   518  		if v != keyword {
   519  			// Retrieve the run again to get its current status.
   520  			r, err = b.client.Runs.Read(stopCtx, r.ID)
   521  			if err != nil {
   522  				return generalError("Failed to retrieve run", err)
   523  			}
   524  
   525  			// Make sure we discard the run if possible.
   526  			if r.Actions.IsDiscardable {
   527  				err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
   528  				if err != nil {
   529  					if op.PlanMode == plans.DestroyMode {
   530  						return generalError("Failed to discard destroy", err)
   531  					}
   532  					return generalError("Failed to discard apply", err)
   533  				}
   534  			}
   535  
   536  			// Even if the run was discarded successfully, we still
   537  			// return an error as the apply command was canceled.
   538  			if op.PlanMode == plans.DestroyMode {
   539  				return errDestroyDiscarded
   540  			}
   541  			return errApplyDiscarded
   542  		}
   543  
   544  		return nil
   545  	}()
   546  
   547  	return <-result
   548  }
   549  
   550  // This method will fetch the redacted plan output and marshal the response into
   551  // a struct the jsonformat.Renderer expects.
   552  //
   553  // Note: Apologies for the lengthy definition, this is a result of not being able to mock receiver methods
   554  var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat.Plan, error) = func(ctx context.Context, baseURL url.URL, token string, planID string) (*jsonformat.Plan, error) {
   555  	client := retryablehttp.NewClient()
   556  	client.RetryMax = 10
   557  	client.RetryWaitMin = 100 * time.Millisecond
   558  	client.RetryWaitMax = 400 * time.Millisecond
   559  	client.Logger = logging.HCLogger()
   560  
   561  	u, err := baseURL.Parse(fmt.Sprintf(
   562  		"plans/%s/json-output-redacted", url.QueryEscape(planID)))
   563  	if err != nil {
   564  		return nil, err
   565  	}
   566  
   567  	req, err := retryablehttp.NewRequest("GET", u.String(), nil)
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  
   572  	req.Header.Set("Authorization", "Bearer "+token)
   573  	req.Header.Set("Accept", "application/json")
   574  
   575  	p := &jsonformat.Plan{}
   576  	resp, err := client.Do(req)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	defer resp.Body.Close()
   581  
   582  	if err = checkResponseCode(resp); err != nil {
   583  		return nil, err
   584  	}
   585  
   586  	if err := json.NewDecoder(resp.Body).Decode(p); err != nil {
   587  		return nil, err
   588  	}
   589  
   590  	return p, nil
   591  }
   592  
   593  func checkResponseCode(r *http.Response) error {
   594  	if r.StatusCode >= 200 && r.StatusCode <= 299 {
   595  		return nil
   596  	}
   597  
   598  	var errs []string
   599  	var err error
   600  
   601  	switch r.StatusCode {
   602  	case 401:
   603  		return tfe.ErrUnauthorized
   604  	case 404:
   605  		return tfe.ErrResourceNotFound
   606  	}
   607  
   608  	errs, err = decodeErrorPayload(r)
   609  	if err != nil {
   610  		return err
   611  	}
   612  
   613  	return errors.New(strings.Join(errs, "\n"))
   614  }
   615  
   616  func decodeErrorPayload(r *http.Response) ([]string, error) {
   617  	// Decode the error payload.
   618  	var errs []string
   619  	errPayload := &jsonapi.ErrorsPayload{}
   620  	err := json.NewDecoder(r.Body).Decode(errPayload)
   621  	if err != nil || len(errPayload.Errors) == 0 {
   622  		return errs, errors.New(r.Status)
   623  	}
   624  
   625  	// Parse and format the errors.
   626  	for _, e := range errPayload.Errors {
   627  		if e.Detail == "" {
   628  			errs = append(errs, e.Title)
   629  		} else {
   630  			errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
   631  		}
   632  	}
   633  
   634  	return errs, nil
   635  }