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