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