github.com/pdecat/terraform@v0.11.9-beta1/backend/remote/backend_apply.go (about)

     1  package remote
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"strings"
    10  
    11  	tfe "github.com/hashicorp/go-tfe"
    12  	"github.com/hashicorp/terraform/backend"
    13  	"github.com/hashicorp/terraform/terraform"
    14  )
    15  
    16  func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation) (*tfe.Run, error) {
    17  	log.Printf("[INFO] backend/remote: starting Apply operation")
    18  
    19  	// Retrieve the workspace used to run this operation in.
    20  	w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
    21  	if err != nil {
    22  		return nil, generalError("error retrieving workspace", err)
    23  	}
    24  
    25  	if !w.Permissions.CanUpdate {
    26  		return nil, fmt.Errorf(strings.TrimSpace(applyErrNoUpdateRights))
    27  	}
    28  
    29  	if w.VCSRepo != nil {
    30  		return nil, fmt.Errorf(strings.TrimSpace(applyErrVCSNotSupported))
    31  	}
    32  
    33  	if op.Parallelism != defaultParallelism {
    34  		return nil, fmt.Errorf(strings.TrimSpace(applyErrParallelismNotSupported))
    35  	}
    36  
    37  	if op.Plan != nil {
    38  		return nil, fmt.Errorf(strings.TrimSpace(applyErrPlanNotSupported))
    39  	}
    40  
    41  	if !op.PlanRefresh {
    42  		return nil, fmt.Errorf(strings.TrimSpace(applyErrNoRefreshNotSupported))
    43  	}
    44  
    45  	if op.Targets != nil {
    46  		return nil, fmt.Errorf(strings.TrimSpace(applyErrTargetsNotSupported))
    47  	}
    48  
    49  	if op.Variables != nil {
    50  		return nil, fmt.Errorf(strings.TrimSpace(
    51  			fmt.Sprintf(applyErrVariablesNotSupported, b.hostname, b.organization, op.Workspace)))
    52  	}
    53  
    54  	if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy {
    55  		return nil, fmt.Errorf(strings.TrimSpace(applyErrNoConfig))
    56  	}
    57  
    58  	// Run the plan phase.
    59  	r, err := b.plan(stopCtx, cancelCtx, op, w)
    60  	if err != nil {
    61  		return r, err
    62  	}
    63  
    64  	// Retrieve the run to get its current status.
    65  	r, err = b.client.Runs.Read(stopCtx, r.ID)
    66  	if err != nil {
    67  		return r, generalError("error retrieving run", err)
    68  	}
    69  
    70  	// Return if there are no changes or the run errored. We return
    71  	// without an error, even if the run errored, as the error is
    72  	// already displayed by the output of the remote run.
    73  	if !r.HasChanges || r.Status == tfe.RunErrored {
    74  		return r, nil
    75  	}
    76  
    77  	// Check any configured sentinel policies.
    78  	if len(r.PolicyChecks) > 0 {
    79  		err = b.checkPolicy(stopCtx, cancelCtx, op, r)
    80  		if err != nil {
    81  			return r, err
    82  		}
    83  	}
    84  
    85  	// Retrieve the run to get its current status.
    86  	r, err = b.client.Runs.Read(stopCtx, r.ID)
    87  	if err != nil {
    88  		return r, generalError("error retrieving run", err)
    89  	}
    90  
    91  	// Return if the run cannot be confirmed.
    92  	if !w.AutoApply && !r.Actions.IsConfirmable {
    93  		return r, nil
    94  	}
    95  
    96  	// Since we already checked the permissions before creating the run
    97  	// this should never happen. But it doesn't hurt to keep this in as
    98  	// a safeguard for any unexpected situations.
    99  	if !w.AutoApply && !r.Permissions.CanApply {
   100  		// Make sure we discard the run if possible.
   101  		if r.Actions.IsDiscardable {
   102  			err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
   103  			if err != nil {
   104  				if op.Destroy {
   105  					return r, generalError("error disarding destroy", err)
   106  				}
   107  				return r, generalError("error disarding apply", err)
   108  			}
   109  		}
   110  		return r, fmt.Errorf(strings.TrimSpace(
   111  			fmt.Sprintf(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace)))
   112  	}
   113  
   114  	mustConfirm := (op.UIIn != nil && op.UIOut != nil) &&
   115  		((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove))
   116  
   117  	if !w.AutoApply {
   118  		if mustConfirm {
   119  			opts := &terraform.InputOpts{Id: "approve"}
   120  
   121  			if op.Destroy {
   122  				opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
   123  				opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
   124  					"There is no undo. Only 'yes' will be accepted to confirm."
   125  			} else {
   126  				opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
   127  				opts.Description = "Terraform will perform the actions described above.\n" +
   128  					"Only 'yes' will be accepted to approve."
   129  			}
   130  
   131  			if err = b.confirm(stopCtx, op, opts, r, "yes"); err != nil {
   132  				return r, err
   133  			}
   134  		}
   135  
   136  		err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{})
   137  		if err != nil {
   138  			return r, generalError("error approving the apply command", err)
   139  		}
   140  	}
   141  
   142  	// If we don't need to ask for confirmation, insert a blank
   143  	// line to separate the ouputs.
   144  	if w.AutoApply || !mustConfirm {
   145  		if b.CLI != nil {
   146  			b.CLI.Output("")
   147  		}
   148  	}
   149  
   150  	r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w)
   151  	if err != nil {
   152  		return r, err
   153  	}
   154  
   155  	logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID)
   156  	if err != nil {
   157  		return r, generalError("error retrieving logs", err)
   158  	}
   159  	scanner := bufio.NewScanner(logs)
   160  
   161  	skip := 0
   162  	for scanner.Scan() {
   163  		// Skip the first 3 lines to prevent duplicate output.
   164  		if skip < 3 {
   165  			skip++
   166  			continue
   167  		}
   168  		if b.CLI != nil {
   169  			b.CLI.Output(b.Colorize().Color(scanner.Text()))
   170  		}
   171  	}
   172  	if err := scanner.Err(); err != nil {
   173  		return r, generalError("error reading logs", err)
   174  	}
   175  
   176  	return r, nil
   177  }
   178  
   179  func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   180  	if b.CLI != nil {
   181  		b.CLI.Output("\n------------------------------------------------------------------------\n")
   182  	}
   183  	for _, pc := range r.PolicyChecks {
   184  		logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID)
   185  		if err != nil {
   186  			return generalError("error retrieving policy check logs", err)
   187  		}
   188  		scanner := bufio.NewScanner(logs)
   189  
   190  		// Retrieve the policy check to get its current status.
   191  		pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID)
   192  		if err != nil {
   193  			return generalError("error retrieving policy check", err)
   194  		}
   195  
   196  		var msgPrefix string
   197  		switch pc.Scope {
   198  		case tfe.PolicyScopeOrganization:
   199  			msgPrefix = "Organization policy check"
   200  		case tfe.PolicyScopeWorkspace:
   201  			msgPrefix = "Workspace policy check"
   202  		default:
   203  			msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope)
   204  		}
   205  
   206  		if b.CLI != nil {
   207  			b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
   208  		}
   209  
   210  		for scanner.Scan() {
   211  			if b.CLI != nil {
   212  				b.CLI.Output(b.Colorize().Color(scanner.Text()))
   213  			}
   214  		}
   215  		if err := scanner.Err(); err != nil {
   216  			return generalError("error reading logs", err)
   217  		}
   218  
   219  		switch pc.Status {
   220  		case tfe.PolicyPasses:
   221  			if b.CLI != nil {
   222  				b.CLI.Output("\n------------------------------------------------------------------------")
   223  			}
   224  			continue
   225  		case tfe.PolicyErrored:
   226  			return fmt.Errorf(msgPrefix + " errored.")
   227  		case tfe.PolicyHardFailed:
   228  			return fmt.Errorf(msgPrefix + " hard failed.")
   229  		case tfe.PolicySoftFailed:
   230  			if op.UIOut == nil || op.UIIn == nil || op.AutoApprove ||
   231  				!pc.Actions.IsOverridable || !pc.Permissions.CanOverride {
   232  				return fmt.Errorf(msgPrefix + " soft failed.")
   233  			}
   234  		default:
   235  			return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status)
   236  		}
   237  
   238  		opts := &terraform.InputOpts{
   239  			Id:          "override",
   240  			Query:       "\nDo you want to override the soft failed policy check?",
   241  			Description: "Only 'override' will be accepted to override.",
   242  		}
   243  
   244  		if err = b.confirm(stopCtx, op, opts, r, "override"); err != nil {
   245  			return err
   246  		}
   247  
   248  		if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil {
   249  			return generalError("error overriding policy check", err)
   250  		}
   251  
   252  		if b.CLI != nil {
   253  			b.CLI.Output("------------------------------------------------------------------------")
   254  		}
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error {
   261  	v, err := op.UIIn.Input(opts)
   262  	if err != nil {
   263  		return fmt.Errorf("Error asking %s: %v", opts.Id, err)
   264  	}
   265  	if v != keyword {
   266  		// Retrieve the run again to get its current status.
   267  		r, err = b.client.Runs.Read(stopCtx, r.ID)
   268  		if err != nil {
   269  			return generalError("error retrieving run", err)
   270  		}
   271  
   272  		// Make sure we discard the run if possible.
   273  		if r.Actions.IsDiscardable {
   274  			err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
   275  			if err != nil {
   276  				if op.Destroy {
   277  					return generalError("error disarding destroy", err)
   278  				}
   279  				return generalError("error disarding apply", err)
   280  			}
   281  		}
   282  
   283  		// Even if the run was disarding successfully, we still
   284  		// return an error as the apply command was cancelled.
   285  		if op.Destroy {
   286  			return errors.New("Destroy discarded.")
   287  		}
   288  		return errors.New("Apply discarded.")
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  const applyErrNoUpdateRights = `
   295  Insufficient rights to apply changes!
   296  
   297  [reset][yellow]The provided credentials have insufficient rights to apply changes. In order
   298  to apply changes at least write permissions on the workspace are required.[reset]
   299  `
   300  
   301  const applyErrVCSNotSupported = `
   302  Apply not allowed for workspaces with a VCS connection.
   303  
   304  A workspace that is connected to a VCS requires the VCS-driven workflow
   305  to ensure that the VCS remains the single source of truth.
   306  `
   307  
   308  const applyErrParallelismNotSupported = `
   309  Custom parallelism values are currently not supported!
   310  
   311  The "remote" backend does not support setting a custom parallelism
   312  value at this time.
   313  `
   314  
   315  const applyErrPlanNotSupported = `
   316  Applying a saved plan is currently not supported!
   317  
   318  The "remote" backend currently requires configuration to be present and
   319  does not accept an existing saved plan as an argument at this time.
   320  `
   321  
   322  const applyErrNoRefreshNotSupported = `
   323  Applying without refresh is currently not supported!
   324  
   325  Currently the "remote" backend will always do an in-memory refresh of
   326  the Terraform state prior to generating the plan.
   327  `
   328  
   329  const applyErrTargetsNotSupported = `
   330  Resource targeting is currently not supported!
   331  
   332  The "remote" backend does not support resource targeting at this time.
   333  `
   334  
   335  const applyErrVariablesNotSupported = `
   336  Run variables are currently not supported!
   337  
   338  The "remote" backend does not support setting run variables at this time.
   339  Currently the only to way to pass variables to the remote backend is by
   340  creating a '*.auto.tfvars' variables file. This file will automatically
   341  be loaded by the "remote" backend when the workspace is configured to use
   342  Terraform v0.10.0 or later.
   343  
   344  Additionally you can also set variables on the workspace in the web UI:
   345  https://%s/app/%s/%s/variables
   346  `
   347  
   348  const applyErrNoConfig = `
   349  No configuration files found!
   350  
   351  Apply requires configuration to be present. Applying without a configuration
   352  would mark everything for destruction, which is normally not what is desired.
   353  If you would like to destroy everything, please run 'terraform destroy' which
   354  does not require any configuration files.
   355  `
   356  
   357  const applyErrNoApplyRights = `
   358  Insufficient rights to approve the pending changes!
   359  
   360  [reset][yellow]There are pending changes, but the provided credentials have insufficient rights
   361  to approve them. The run will be discarded to prevent it from blocking the queue
   362  waiting for external approval. To queue a run that can be approved by someone
   363  else, please use the 'Queue Plan' button in the web UI:
   364  https://%s/app/%s/%s/runs[reset]
   365  `
   366  
   367  const applyDefaultHeader = `
   368  [reset][yellow]Running apply in the remote backend. Output will stream here. Pressing Ctrl-C
   369  will cancel the remote apply if its still pending. If the apply started it
   370  will stop streaming the logs, but will not stop the apply running remotely.
   371  To view this run in a browser, visit:
   372  https://%s/app/%s/%s/runs/%s[reset]
   373  `