github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote/backend_apply.go (about)

     1  package remote
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  
    10  	tfe "github.com/hashicorp/go-tfe"
    11  	version "github.com/hashicorp/go-version"
    12  	"github.com/hashicorp/terraform/backend"
    13  	"github.com/hashicorp/terraform/terraform"
    14  	"github.com/hashicorp/terraform/tfdiags"
    15  )
    16  
    17  func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
    18  	log.Printf("[INFO] backend/remote: starting Apply operation")
    19  
    20  	var diags tfdiags.Diagnostics
    21  
    22  	// We should remove the `CanUpdate` part of this test, but for now
    23  	// (to remain compatible with tfe.v2.1) we'll leave it in here.
    24  	if !w.Permissions.CanUpdate && !w.Permissions.CanQueueApply {
    25  		diags = diags.Append(tfdiags.Sourceless(
    26  			tfdiags.Error,
    27  			"Insufficient rights to apply changes",
    28  			"The provided credentials have insufficient rights to apply changes. In order "+
    29  				"to apply changes at least write permissions on the workspace are required.",
    30  		))
    31  		return nil, diags.Err()
    32  	}
    33  
    34  	if w.VCSRepo != nil {
    35  		diags = diags.Append(tfdiags.Sourceless(
    36  			tfdiags.Error,
    37  			"Apply not allowed for workspaces with a VCS connection",
    38  			"A workspace that is connected to a VCS requires the VCS-driven workflow "+
    39  				"to ensure that the VCS remains the single source of truth.",
    40  		))
    41  		return nil, diags.Err()
    42  	}
    43  
    44  	if op.Parallelism != defaultParallelism {
    45  		diags = diags.Append(tfdiags.Sourceless(
    46  			tfdiags.Error,
    47  			"Custom parallelism values are currently not supported",
    48  			`The "remote" backend does not support setting a custom parallelism `+
    49  				`value at this time.`,
    50  		))
    51  	}
    52  
    53  	if op.PlanFile != nil {
    54  		diags = diags.Append(tfdiags.Sourceless(
    55  			tfdiags.Error,
    56  			"Applying a saved plan is currently not supported",
    57  			`The "remote" backend currently requires configuration to be present and `+
    58  				`does not accept an existing saved plan as an argument at this time.`,
    59  		))
    60  	}
    61  
    62  	if !op.PlanRefresh {
    63  		diags = diags.Append(tfdiags.Sourceless(
    64  			tfdiags.Error,
    65  			"Applying without refresh is currently not supported",
    66  			`Currently the "remote" backend will always do an in-memory refresh of `+
    67  				`the Terraform state prior to generating the plan.`,
    68  		))
    69  	}
    70  
    71  	if b.hasExplicitVariableValues(op) {
    72  		diags = diags.Append(tfdiags.Sourceless(
    73  			tfdiags.Error,
    74  			"Run variables are currently not supported",
    75  			fmt.Sprintf(
    76  				"The \"remote\" backend does not support setting run variables at this time. "+
    77  					"Currently the only to way to pass variables to the remote backend is by "+
    78  					"creating a '*.auto.tfvars' variables file. This file will automatically "+
    79  					"be loaded by the \"remote\" backend when the workspace is configured to use "+
    80  					"Terraform v0.10.0 or later.\n\nAdditionally you can also set variables on "+
    81  					"the workspace in the web UI:\nhttps://%s/app/%s/%s/variables",
    82  				b.hostname, b.organization, op.Workspace,
    83  			),
    84  		))
    85  	}
    86  
    87  	if !op.HasConfig() && !op.Destroy {
    88  		diags = diags.Append(tfdiags.Sourceless(
    89  			tfdiags.Error,
    90  			"No configuration files found",
    91  			`Apply requires configuration to be present. Applying without a configuration `+
    92  				`would mark everything for destruction, which is normally not what is desired. `+
    93  				`If you would like to destroy everything, please run 'terraform destroy' which `+
    94  				`does not require any configuration files.`,
    95  		))
    96  	}
    97  
    98  	if len(op.Targets) != 0 {
    99  		// For API versions prior to 2.3, RemoteAPIVersion will return an empty string,
   100  		// so if there's an error when parsing the RemoteAPIVersion, it's handled as
   101  		// equivalent to an API version < 2.3.
   102  		currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion())
   103  		desiredAPIVersion, _ := version.NewVersion("2.3")
   104  
   105  		if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) {
   106  			diags = diags.Append(tfdiags.Sourceless(
   107  				tfdiags.Error,
   108  				"Resource targeting is not supported",
   109  				fmt.Sprintf(
   110  					`The host %s does not support the -target option for `+
   111  						`remote plans.`,
   112  					b.hostname,
   113  				),
   114  			))
   115  		}
   116  	}
   117  
   118  	// Return if there are any errors.
   119  	if diags.HasErrors() {
   120  		return nil, diags.Err()
   121  	}
   122  
   123  	// Run the plan phase.
   124  	r, err := b.plan(stopCtx, cancelCtx, op, w)
   125  	if err != nil {
   126  		return r, err
   127  	}
   128  
   129  	// This check is also performed in the plan method to determine if
   130  	// the policies should be checked, but we need to check the values
   131  	// here again to determine if we are done and should return.
   132  	if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored {
   133  		return r, nil
   134  	}
   135  
   136  	// Retrieve the run to get its current status.
   137  	r, err = b.client.Runs.Read(stopCtx, r.ID)
   138  	if err != nil {
   139  		return r, generalError("Failed to retrieve run", err)
   140  	}
   141  
   142  	// Return if the run cannot be confirmed.
   143  	if !w.AutoApply && !r.Actions.IsConfirmable {
   144  		return r, nil
   145  	}
   146  
   147  	// Since we already checked the permissions before creating the run
   148  	// this should never happen. But it doesn't hurt to keep this in as
   149  	// a safeguard for any unexpected situations.
   150  	if !w.AutoApply && !r.Permissions.CanApply {
   151  		// Make sure we discard the run if possible.
   152  		if r.Actions.IsDiscardable {
   153  			err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
   154  			if err != nil {
   155  				if op.Destroy {
   156  					return r, generalError("Failed to discard destroy", err)
   157  				}
   158  				return r, generalError("Failed to discard apply", err)
   159  			}
   160  		}
   161  		diags = diags.Append(tfdiags.Sourceless(
   162  			tfdiags.Error,
   163  			"Insufficient rights to approve the pending changes",
   164  			fmt.Sprintf("There are pending changes, but the provided credentials have "+
   165  				"insufficient rights to approve them. The run will be discarded to prevent "+
   166  				"it from blocking the queue waiting for external approval. To queue a run "+
   167  				"that can be approved by someone else, please use the 'Queue Plan' button in "+
   168  				"the web UI:\nhttps://%s/app/%s/%s/runs", b.hostname, b.organization, op.Workspace),
   169  		))
   170  		return r, diags.Err()
   171  	}
   172  
   173  	mustConfirm := (op.UIIn != nil && op.UIOut != nil) &&
   174  		((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove))
   175  
   176  	if !w.AutoApply {
   177  		if mustConfirm {
   178  			opts := &terraform.InputOpts{Id: "approve"}
   179  
   180  			if op.Destroy {
   181  				opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
   182  				opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
   183  					"There is no undo. Only 'yes' will be accepted to confirm."
   184  			} else {
   185  				opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
   186  				opts.Description = "Terraform will perform the actions described above.\n" +
   187  					"Only 'yes' will be accepted to approve."
   188  			}
   189  
   190  			err = b.confirm(stopCtx, op, opts, r, "yes")
   191  			if err != nil && err != errRunApproved {
   192  				return r, err
   193  			}
   194  		}
   195  
   196  		if err != errRunApproved {
   197  			if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
   198  				return r, generalError("Failed to approve the apply command", err)
   199  			}
   200  		}
   201  	}
   202  
   203  	// If we don't need to ask for confirmation, insert a blank
   204  	// line to separate the ouputs.
   205  	if w.AutoApply || !mustConfirm {
   206  		if b.CLI != nil {
   207  			b.CLI.Output("")
   208  		}
   209  	}
   210  
   211  	r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w)
   212  	if err != nil {
   213  		return r, err
   214  	}
   215  
   216  	logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID)
   217  	if err != nil {
   218  		return r, generalError("Failed to retrieve logs", err)
   219  	}
   220  	reader := bufio.NewReaderSize(logs, 64*1024)
   221  
   222  	if b.CLI != nil {
   223  		skip := 0
   224  		for next := true; next; {
   225  			var l, line []byte
   226  
   227  			for isPrefix := true; isPrefix; {
   228  				l, isPrefix, err = reader.ReadLine()
   229  				if err != nil {
   230  					if err != io.EOF {
   231  						return r, generalError("Failed to read logs", err)
   232  					}
   233  					next = false
   234  				}
   235  				line = append(line, l...)
   236  			}
   237  
   238  			// Skip the first 3 lines to prevent duplicate output.
   239  			if skip < 3 {
   240  				skip++
   241  				continue
   242  			}
   243  
   244  			if next || len(line) > 0 {
   245  				b.CLI.Output(b.Colorize().Color(string(line)))
   246  			}
   247  		}
   248  	}
   249  
   250  	return r, nil
   251  }
   252  
   253  const applyDefaultHeader = `
   254  [reset][yellow]Running apply in the remote backend. Output will stream here. Pressing Ctrl-C
   255  will cancel the remote apply if it's still pending. If the apply started it
   256  will stop streaming the logs, but will not stop the apply running remotely.[reset]
   257  
   258  Preparing the remote apply...
   259  `