github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/backend_apply.go (about)

     1  package cloud
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"log"
     9  
    10  	tfe "github.com/hashicorp/go-tfe"
    11  	"github.com/hashicorp/terraform/internal/backend"
    12  	"github.com/hashicorp/terraform/internal/command/jsonformat"
    13  	"github.com/hashicorp/terraform/internal/plans"
    14  	"github.com/hashicorp/terraform/internal/terraform"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  )
    17  
    18  func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
    19  	log.Printf("[INFO] cloud: 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  			`Terraform Cloud 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  			`Terraform Cloud 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 !op.HasConfig() && op.PlanMode != plans.DestroyMode {
    64  		diags = diags.Append(tfdiags.Sourceless(
    65  			tfdiags.Error,
    66  			"No configuration files found",
    67  			`Apply requires configuration to be present. Applying without a configuration `+
    68  				`would mark everything for destruction, which is normally not what is desired. `+
    69  				`If you would like to destroy everything, please run 'terraform destroy' which `+
    70  				`does not require any configuration files.`,
    71  		))
    72  	}
    73  
    74  	// Return if there are any errors.
    75  	if diags.HasErrors() {
    76  		return nil, diags.Err()
    77  	}
    78  
    79  	// Run the plan phase.
    80  	r, err := b.plan(stopCtx, cancelCtx, op, w)
    81  	if err != nil {
    82  		return r, err
    83  	}
    84  
    85  	// This check is also performed in the plan method to determine if
    86  	// the policies should be checked, but we need to check the values
    87  	// here again to determine if we are done and should return.
    88  	if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored {
    89  		return r, nil
    90  	}
    91  
    92  	// Retrieve the run to get its current status.
    93  	r, err = b.client.Runs.Read(stopCtx, r.ID)
    94  	if err != nil {
    95  		return r, generalError("Failed to retrieve run", err)
    96  	}
    97  
    98  	// Return if the run cannot be confirmed.
    99  	if !op.AutoApprove && !r.Actions.IsConfirmable {
   100  		return r, nil
   101  	}
   102  
   103  	mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove
   104  
   105  	if mustConfirm && b.input {
   106  		opts := &terraform.InputOpts{Id: "approve"}
   107  
   108  		if op.PlanMode == plans.DestroyMode {
   109  			opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
   110  			opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
   111  				"There is no undo. Only 'yes' will be accepted to confirm."
   112  		} else {
   113  			opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
   114  			opts.Description = "Terraform will perform the actions described above.\n" +
   115  				"Only 'yes' will be accepted to approve."
   116  		}
   117  
   118  		err = b.confirm(stopCtx, op, opts, r, "yes")
   119  		if err != nil && err != errRunApproved {
   120  			return r, err
   121  		}
   122  	} else if mustConfirm && !b.input {
   123  		return r, errApplyNeedsUIConfirmation
   124  	} else {
   125  		// If we don't need to ask for confirmation, insert a blank
   126  		// line to separate the ouputs.
   127  		if b.CLI != nil {
   128  			b.CLI.Output("")
   129  		}
   130  	}
   131  
   132  	if !op.AutoApprove && err != errRunApproved {
   133  		if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil {
   134  			return r, generalError("Failed to approve the apply command", err)
   135  		}
   136  	}
   137  
   138  	// Retrieve the run to get task stages.
   139  	// Task Stages are calculated upfront so we only need to call this once for the run.
   140  	taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID)
   141  	if err != nil {
   142  		return r, err
   143  	}
   144  
   145  	if stage, ok := taskStages[tfe.PreApply]; ok {
   146  		if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-apply Tasks"); err != nil {
   147  			return r, err
   148  		}
   149  	}
   150  
   151  	r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w)
   152  	if err != nil {
   153  		return r, err
   154  	}
   155  
   156  	err = b.renderApplyLogs(stopCtx, r)
   157  	if err != nil {
   158  		return r, err
   159  	}
   160  
   161  	return r, nil
   162  }
   163  
   164  func (b *Cloud) renderApplyLogs(ctx context.Context, run *tfe.Run) error {
   165  	logs, err := b.client.Applies.Logs(ctx, run.Apply.ID)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	if b.CLI != nil {
   171  		reader := bufio.NewReaderSize(logs, 64*1024)
   172  		skip := 0
   173  
   174  		for next := true; next; {
   175  			var l, line []byte
   176  			var err error
   177  
   178  			for isPrefix := true; isPrefix; {
   179  				l, isPrefix, err = reader.ReadLine()
   180  				if err != nil {
   181  					if err != io.EOF {
   182  						return generalError("Failed to read logs", err)
   183  					}
   184  					next = false
   185  				}
   186  
   187  				line = append(line, l...)
   188  			}
   189  
   190  			// Apply logs show the same Terraform info logs as shown in the plan logs
   191  			// (which contain version and os/arch information), we therefore skip to prevent duplicate output.
   192  			if skip < 3 {
   193  				skip++
   194  				continue
   195  			}
   196  
   197  			if next || len(line) > 0 {
   198  				log := &jsonformat.JSONLog{}
   199  				if err := json.Unmarshal(line, log); err != nil {
   200  					// If we can not parse the line as JSON, we will simply
   201  					// print the line. This maintains backwards compatibility for
   202  					// users who do not wish to enable structured output in their
   203  					// workspace.
   204  					b.CLI.Output(string(line))
   205  					continue
   206  				}
   207  
   208  				if b.renderer != nil {
   209  					// Otherwise, we will print the log
   210  					err := b.renderer.RenderLog(log)
   211  					if err != nil {
   212  						return err
   213  					}
   214  				}
   215  			}
   216  		}
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  const applyDefaultHeader = `
   223  [reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C
   224  will cancel the remote apply if it's still pending. If the apply started it
   225  will stop streaming the logs, but will not stop the apply running remotely.[reset]
   226  
   227  Preparing the remote apply...
   228  `