github.com/mvisonneau/terraform@v0.11.12-beta1/backend/remote/backend_plan.go (about)

     1  package remote
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  	"time"
    15  
    16  	tfe "github.com/hashicorp/go-tfe"
    17  	"github.com/hashicorp/terraform/backend"
    18  )
    19  
    20  func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
    21  	log.Printf("[INFO] backend/remote: starting Plan operation")
    22  
    23  	if !w.Permissions.CanQueueRun {
    24  		return nil, fmt.Errorf(strings.TrimSpace(fmt.Sprintf(planErrNoQueueRunRights)))
    25  	}
    26  
    27  	if op.ModuleDepth != defaultModuleDepth {
    28  		return nil, fmt.Errorf(strings.TrimSpace(planErrModuleDepthNotSupported))
    29  	}
    30  
    31  	if op.Parallelism != defaultParallelism {
    32  		return nil, fmt.Errorf(strings.TrimSpace(planErrParallelismNotSupported))
    33  	}
    34  
    35  	if op.Plan != nil {
    36  		return nil, fmt.Errorf(strings.TrimSpace(planErrPlanNotSupported))
    37  	}
    38  
    39  	if op.PlanOutPath != "" {
    40  		return nil, fmt.Errorf(strings.TrimSpace(planErrOutPathNotSupported))
    41  	}
    42  
    43  	if !op.PlanRefresh {
    44  		return nil, fmt.Errorf(strings.TrimSpace(planErrNoRefreshNotSupported))
    45  	}
    46  
    47  	if op.Targets != nil {
    48  		return nil, fmt.Errorf(strings.TrimSpace(planErrTargetsNotSupported))
    49  	}
    50  
    51  	if op.Variables != nil {
    52  		return nil, fmt.Errorf(strings.TrimSpace(
    53  			fmt.Sprintf(planErrVariablesNotSupported, b.hostname, b.organization, op.Workspace)))
    54  	}
    55  
    56  	if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy {
    57  		return nil, fmt.Errorf(strings.TrimSpace(planErrNoConfig))
    58  	}
    59  
    60  	return b.plan(stopCtx, cancelCtx, op, w)
    61  }
    62  
    63  func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
    64  	if b.CLI != nil {
    65  		header := planDefaultHeader
    66  		if op.Type == backend.OperationTypeApply {
    67  			header = applyDefaultHeader
    68  		}
    69  		b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n"))
    70  	}
    71  
    72  	configOptions := tfe.ConfigurationVersionCreateOptions{
    73  		AutoQueueRuns: tfe.Bool(false),
    74  		Speculative:   tfe.Bool(op.Type == backend.OperationTypePlan),
    75  	}
    76  
    77  	cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions)
    78  	if err != nil {
    79  		return nil, generalError("error creating configuration version", err)
    80  	}
    81  
    82  	var configDir string
    83  	if op.Module != nil && op.Module.Config().Dir != "" {
    84  		// Make sure to take the working directory into account by removing
    85  		// the working directory from the current path. This will result in
    86  		// a path that points to the expected root of the workspace.
    87  		configDir = filepath.Clean(strings.TrimSuffix(
    88  			filepath.Clean(op.Module.Config().Dir),
    89  			filepath.Clean(w.WorkingDirectory),
    90  		))
    91  	} else {
    92  		// We did a check earlier to make sure we either have a config dir,
    93  		// or the plan is run with -destroy. So this else clause will only
    94  		// be executed when we are destroying and doesn't need the config.
    95  		configDir, err = ioutil.TempDir("", "tf")
    96  		if err != nil {
    97  			return nil, generalError("error creating temporary directory", err)
    98  		}
    99  		defer os.RemoveAll(configDir)
   100  
   101  		// Make sure the configured working directory exists.
   102  		err = os.MkdirAll(filepath.Join(configDir, w.WorkingDirectory), 0700)
   103  		if err != nil {
   104  			return nil, generalError(
   105  				"error creating temporary working directory", err)
   106  		}
   107  	}
   108  
   109  	err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir)
   110  	if err != nil {
   111  		return nil, generalError("error uploading configuration files", err)
   112  	}
   113  
   114  	uploaded := false
   115  	for i := 0; i < 60 && !uploaded; i++ {
   116  		select {
   117  		case <-stopCtx.Done():
   118  			return nil, context.Canceled
   119  		case <-cancelCtx.Done():
   120  			return nil, context.Canceled
   121  		case <-time.After(500 * time.Millisecond):
   122  			cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID)
   123  			if err != nil {
   124  				return nil, generalError("error retrieving configuration version", err)
   125  			}
   126  
   127  			if cv.Status == tfe.ConfigurationUploaded {
   128  				uploaded = true
   129  			}
   130  		}
   131  	}
   132  
   133  	if !uploaded {
   134  		return nil, generalError(
   135  			"error uploading configuration files", errors.New("operation timed out"))
   136  	}
   137  
   138  	runOptions := tfe.RunCreateOptions{
   139  		IsDestroy:            tfe.Bool(op.Destroy),
   140  		Message:              tfe.String("Queued manually using Terraform"),
   141  		ConfigurationVersion: cv,
   142  		Workspace:            w,
   143  	}
   144  
   145  	r, err := b.client.Runs.Create(stopCtx, runOptions)
   146  	if err != nil {
   147  		return r, generalError("error creating run", err)
   148  	}
   149  
   150  	// When the lock timeout is set,
   151  	if op.StateLockTimeout > 0 {
   152  		go func() {
   153  			select {
   154  			case <-stopCtx.Done():
   155  				return
   156  			case <-cancelCtx.Done():
   157  				return
   158  			case <-time.After(op.StateLockTimeout):
   159  				// Retrieve the run to get its current status.
   160  				r, err := b.client.Runs.Read(cancelCtx, r.ID)
   161  				if err != nil {
   162  					log.Printf("[ERROR] error reading run: %v", err)
   163  					return
   164  				}
   165  
   166  				if r.Status == tfe.RunPending && r.Actions.IsCancelable {
   167  					if b.CLI != nil {
   168  						b.CLI.Output(b.Colorize().Color(strings.TrimSpace(lockTimeoutErr)))
   169  					}
   170  
   171  					// We abuse the auto aprove flag to indicate that we do not
   172  					// want to ask if the remote operation should be canceled.
   173  					op.AutoApprove = true
   174  
   175  					p, err := os.FindProcess(os.Getpid())
   176  					if err != nil {
   177  						log.Printf("[ERROR] error searching process ID: %v", err)
   178  						return
   179  					}
   180  					p.Signal(syscall.SIGINT)
   181  				}
   182  			}
   183  		}()
   184  	}
   185  
   186  	if b.CLI != nil {
   187  		b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf(
   188  			runHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n"))
   189  	}
   190  
   191  	r, err = b.waitForRun(stopCtx, cancelCtx, op, "plan", r, w)
   192  	if err != nil {
   193  		return r, err
   194  	}
   195  
   196  	logs, err := b.client.Plans.Logs(stopCtx, r.Plan.ID)
   197  	if err != nil {
   198  		return r, generalError("error retrieving logs", err)
   199  	}
   200  	scanner := bufio.NewScanner(logs)
   201  
   202  	for scanner.Scan() {
   203  		if b.CLI != nil {
   204  			b.CLI.Output(b.Colorize().Color(scanner.Text()))
   205  		}
   206  	}
   207  	if err := scanner.Err(); err != nil {
   208  		return r, generalError("error reading logs", err)
   209  	}
   210  
   211  	// Retrieve the run to get its current status.
   212  	r, err = b.client.Runs.Read(stopCtx, r.ID)
   213  	if err != nil {
   214  		return r, generalError("error retrieving run", err)
   215  	}
   216  
   217  	// Return if the run errored. We return without an error, even
   218  	// if the run errored, as the error is already displayed by the
   219  	// output of the remote run.
   220  	if r.Status == tfe.RunErrored {
   221  		return r, nil
   222  	}
   223  
   224  	// Check any configured sentinel policies.
   225  	if len(r.PolicyChecks) > 0 {
   226  		err = b.checkPolicy(stopCtx, cancelCtx, op, r)
   227  		if err != nil {
   228  			return r, err
   229  		}
   230  	}
   231  
   232  	return r, nil
   233  }
   234  
   235  const planErrNoQueueRunRights = `
   236  Insufficient rights to generate a plan!
   237  
   238  [reset][yellow]The provided credentials have insufficient rights to generate a plan. In order
   239  to generate plans, at least plan permissions on the workspace are required.[reset]
   240  `
   241  
   242  const planErrModuleDepthNotSupported = `
   243  Custom module depths are currently not supported!
   244  
   245  The "remote" backend does not support setting a custom module
   246  depth at this time.
   247  `
   248  
   249  const planErrParallelismNotSupported = `
   250  Custom parallelism values are currently not supported!
   251  
   252  The "remote" backend does not support setting a custom parallelism
   253  value at this time.
   254  `
   255  
   256  const planErrPlanNotSupported = `
   257  Displaying a saved plan is currently not supported!
   258  
   259  The "remote" backend currently requires configuration to be present and
   260  does not accept an existing saved plan as an argument at this time.
   261  `
   262  
   263  const planErrOutPathNotSupported = `
   264  Saving a generated plan is currently not supported!
   265  
   266  The "remote" backend does not support saving the generated execution
   267  plan locally at this time.
   268  `
   269  
   270  const planErrNoRefreshNotSupported = `
   271  Planning without refresh is currently not supported!
   272  
   273  Currently the "remote" backend will always do an in-memory refresh of
   274  the Terraform state prior to generating the plan.
   275  `
   276  
   277  const planErrTargetsNotSupported = `
   278  Resource targeting is currently not supported!
   279  
   280  The "remote" backend does not support resource targeting at this time.
   281  `
   282  
   283  const planErrVariablesNotSupported = `
   284  Run variables are currently not supported!
   285  
   286  The "remote" backend does not support setting run variables at this time.
   287  Currently the only to way to pass variables to the remote backend is by
   288  creating a '*.auto.tfvars' variables file. This file will automatically
   289  be loaded by the "remote" backend when the workspace is configured to use
   290  Terraform v0.10.0 or later.
   291  
   292  Additionally you can also set variables on the workspace in the web UI:
   293  https://%s/app/%s/%s/variables
   294  `
   295  
   296  const planErrNoConfig = `
   297  No configuration files found!
   298  
   299  Plan requires configuration to be present. Planning without a configuration
   300  would mark everything for destruction, which is normally not what is desired.
   301  If you would like to destroy everything, please run plan with the "-destroy"
   302  flag or create a single empty configuration file. Otherwise, please create
   303  a Terraform configuration file in the path being executed and try again.
   304  `
   305  
   306  const planDefaultHeader = `
   307  [reset][yellow]Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
   308  will stop streaming the logs, but will not stop the plan running remotely.[reset]
   309  
   310  Preparing the remote plan...
   311  `
   312  
   313  const runHeader = `
   314  [reset][yellow]To view this run in a browser, visit:
   315  https://%s/app/%s/%s/runs/%s[reset]
   316  `
   317  
   318  // The newline in this error is to make it look good in the CLI!
   319  const lockTimeoutErr = `
   320  [reset][red]Lock timeout exceeded, sending interrupt to cancel the remote operation.
   321  [reset]
   322  `