2023-05-02 10:33:06 -05:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2021-08-12 14:30:24 -05:00
package cloud
import (
"bufio"
"context"
2023-02-09 06:35:48 -06:00
"encoding/json"
2021-08-12 14:30:24 -05:00
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
2023-04-11 15:47:56 -05:00
"strconv"
2021-08-12 14:30:24 -05:00
"strings"
"syscall"
"time"
tfe "github.com/hashicorp/go-tfe"
2023-05-31 14:55:35 -05:00
version "github.com/hashicorp/go-version"
2023-08-17 07:45:11 -05:00
"github.com/placeholderplaceholderplaceholder/opentf/internal/backend"
"github.com/placeholderplaceholderplaceholder/opentf/internal/cloud/cloudplan"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/jsonformat"
"github.com/placeholderplaceholderplaceholder/opentf/internal/configs"
"github.com/placeholderplaceholderplaceholder/opentf/internal/genconfig"
"github.com/placeholderplaceholderplaceholder/opentf/internal/plans"
"github.com/placeholderplaceholderplaceholder/opentf/internal/tfdiags"
2021-08-12 14:30:24 -05:00
)
var planConfigurationVersionsPollInterval = 500 * time . Millisecond
func ( b * Cloud ) opPlan ( stopCtx , cancelCtx context . Context , op * backend . Operation , w * tfe . Workspace ) ( * tfe . Run , error ) {
log . Printf ( "[INFO] cloud: starting Plan operation" )
var diags tfdiags . Diagnostics
if ! w . Permissions . CanQueueRun {
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Insufficient rights to generate a plan" ,
"The provided credentials have insufficient rights to generate a plan. In order " +
"to generate plans, at least plan permissions on the workspace are required." ,
) )
return nil , diags . Err ( )
}
2021-09-22 16:59:47 -05:00
if b . ContextOpts != nil && b . ContextOpts . Parallelism != defaultParallelism {
2021-08-12 14:30:24 -05:00
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Custom parallelism values are currently not supported" ,
` Terraform Cloud does not support setting a custom parallelism ` +
` value at this time. ` ,
) )
}
if op . PlanFile != nil {
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Displaying a saved plan is currently not supported" ,
` Terraform Cloud currently requires configuration to be present and ` +
` does not accept an existing saved plan as an argument at this time. ` ,
) )
}
if ! op . HasConfig ( ) && op . PlanMode != plans . DestroyMode {
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"No configuration files found" ,
` Plan requires configuration to be present. Planning without a configuration ` +
` would mark everything for destruction, which is normally not what is desired. ` +
` If you would like to destroy everything, please run plan with the "-destroy" ` +
` flag or create a single empty configuration file. Otherwise, please create ` +
2023-08-22 09:31:53 -05:00
` a OpenTF configuration file in the path being executed and try again. ` ,
2021-08-12 14:30:24 -05:00
) )
}
2023-05-30 02:17:02 -05:00
if len ( op . GenerateConfigOut ) > 0 {
diags = diags . Append ( genconfig . ValidateTargetFile ( op . GenerateConfigOut ) )
}
2021-08-12 14:30:24 -05:00
// Return if there are any errors.
if diags . HasErrors ( ) {
return nil , diags . Err ( )
}
2023-06-21 18:29:28 -05:00
// If the run errored, exit before checking whether to save a plan file
run , err := b . plan ( stopCtx , cancelCtx , op , w )
if err != nil {
return nil , err
}
// Save plan file if -out <FILE> was specified
if op . PlanOutPath != "" {
bookmark := cloudplan . NewSavedPlanBookmark ( run . ID , b . hostname )
err = bookmark . Save ( op . PlanOutPath )
if err != nil {
return nil , err
}
}
// Everything succeded, so display next steps
op . View . PlanNextStep ( op . PlanOutPath , op . GenerateConfigOut )
return run , nil
2021-08-12 14:30:24 -05:00
}
func ( b * Cloud ) plan ( stopCtx , cancelCtx context . Context , op * backend . Operation , w * tfe . Workspace ) ( * tfe . Run , error ) {
if b . CLI != nil {
header := planDefaultHeader
2021-10-08 14:36:37 -05:00
if op . Type == backend . OperationTypeApply || op . Type == backend . OperationTypeRefresh {
2021-08-12 14:30:24 -05:00
header = applyDefaultHeader
}
b . CLI . Output ( b . Colorize ( ) . Color ( strings . TrimSpace ( header ) + "\n" ) )
}
2023-06-21 18:29:28 -05:00
// Plan-only means they ran terraform plan without -out.
2023-07-26 10:31:24 -05:00
provisional := op . PlanOutPath != ""
planOnly := op . Type == backend . OperationTypePlan && ! provisional
2023-06-21 18:29:28 -05:00
2021-08-12 14:30:24 -05:00
configOptions := tfe . ConfigurationVersionCreateOptions {
AutoQueueRuns : tfe . Bool ( false ) ,
2023-06-21 18:29:28 -05:00
Speculative : tfe . Bool ( planOnly ) ,
2023-07-26 10:31:24 -05:00
Provisional : tfe . Bool ( provisional ) ,
2021-08-12 14:30:24 -05:00
}
cv , err := b . client . ConfigurationVersions . Create ( stopCtx , w . ID , configOptions )
if err != nil {
return nil , generalError ( "Failed to create configuration version" , err )
}
var configDir string
if op . ConfigDir != "" {
// De-normalize the configuration directory path.
configDir , err = filepath . Abs ( op . ConfigDir )
if err != nil {
return nil , generalError (
"Failed to get absolute path of the configuration directory: %v" , err )
}
// Make sure to take the working directory into account by removing
// the working directory from the current path. This will result in
// a path that points to the expected root of the workspace.
configDir = filepath . Clean ( strings . TrimSuffix (
filepath . Clean ( configDir ) ,
filepath . Clean ( w . WorkingDirectory ) ,
) )
// If the workspace has a subdirectory as its working directory then
// our configDir will be some parent directory of the current working
// directory. Users are likely to find that surprising, so we'll
// produce an explicit message about it to be transparent about what
// we are doing and why.
if w . WorkingDirectory != "" && filepath . Base ( configDir ) != w . WorkingDirectory {
if b . CLI != nil {
b . CLI . Output ( fmt . Sprintf ( strings . TrimSpace ( `
The remote workspace is configured to work with configuration at
% s relative to the target repository .
2023-08-22 09:31:53 -05:00
OpenTF will upload the contents of the following directory ,
2021-08-12 14:30:24 -05:00
excluding files or directories as defined by a . terraformignore file
at % s / . terraformignore ( if it is present ) ,
in order to capture the filesystem context the remote workspace expects :
% s
` ) , w . WorkingDirectory , configDir , configDir ) + "\n" )
}
}
} else {
// We did a check earlier to make sure we either have a config dir,
// or the plan is run with -destroy. So this else clause will only
// be executed when we are destroying and doesn't need the config.
configDir , err = ioutil . TempDir ( "" , "tf" )
if err != nil {
return nil , generalError ( "Failed to create temporary directory" , err )
}
defer os . RemoveAll ( configDir )
// Make sure the configured working directory exists.
err = os . MkdirAll ( filepath . Join ( configDir , w . WorkingDirectory ) , 0700 )
if err != nil {
return nil , generalError (
"Failed to create temporary working directory" , err )
}
}
err = b . client . ConfigurationVersions . Upload ( stopCtx , cv . UploadURL , configDir )
if err != nil {
return nil , generalError ( "Failed to upload configuration files" , err )
}
uploaded := false
for i := 0 ; i < 60 && ! uploaded ; i ++ {
select {
case <- stopCtx . Done ( ) :
return nil , context . Canceled
case <- cancelCtx . Done ( ) :
return nil , context . Canceled
case <- time . After ( planConfigurationVersionsPollInterval ) :
cv , err = b . client . ConfigurationVersions . Read ( stopCtx , cv . ID )
if err != nil {
return nil , generalError ( "Failed to retrieve configuration version" , err )
}
if cv . Status == tfe . ConfigurationUploaded {
uploaded = true
}
}
}
if ! uploaded {
return nil , generalError (
"Failed to upload configuration files" , errors . New ( "operation timed out" ) )
}
runOptions := tfe . RunCreateOptions {
ConfigurationVersion : cv ,
Refresh : tfe . Bool ( op . PlanRefresh ) ,
Workspace : w ,
2021-09-22 16:53:33 -05:00
AutoApply : tfe . Bool ( op . AutoApprove ) ,
2023-06-21 18:29:28 -05:00
SavePlan : tfe . Bool ( op . PlanOutPath != "" ) ,
2021-08-12 14:30:24 -05:00
}
switch op . PlanMode {
case plans . NormalMode :
// okay, but we don't need to do anything special for this
case plans . RefreshOnlyMode :
runOptions . RefreshOnly = tfe . Bool ( true )
case plans . DestroyMode :
runOptions . IsDestroy = tfe . Bool ( true )
default :
// Shouldn't get here because we should update this for each new
// plan mode we add, mapping it to the corresponding RunCreateOptions
// field.
return nil , generalError (
"Invalid plan mode" ,
fmt . Errorf ( "Terraform Cloud doesn't support %s" , op . PlanMode ) ,
)
}
if len ( op . Targets ) != 0 {
runOptions . TargetAddrs = make ( [ ] string , 0 , len ( op . Targets ) )
for _ , addr := range op . Targets {
runOptions . TargetAddrs = append ( runOptions . TargetAddrs , addr . String ( ) )
}
}
if len ( op . ForceReplace ) != 0 {
runOptions . ReplaceAddrs = make ( [ ] string , 0 , len ( op . ForceReplace ) )
for _ , addr := range op . ForceReplace {
runOptions . ReplaceAddrs = append ( runOptions . ReplaceAddrs , addr . String ( ) )
}
}
2021-08-23 12:35:54 -05:00
config , _ , configDiags := op . ConfigLoader . LoadConfigWithSnapshot ( op . ConfigDir )
if configDiags . HasErrors ( ) {
return nil , fmt . Errorf ( "error loading config with snapshot: %w" , configDiags . Errs ( ) [ 0 ] )
}
2023-05-31 14:55:35 -05:00
2021-08-23 12:35:54 -05:00
variables , varDiags := ParseCloudRunVariables ( op . Variables , config . Module . Variables )
if varDiags . HasErrors ( ) {
return nil , varDiags . Err ( )
}
2022-11-21 11:13:47 -06:00
runVariables := make ( [ ] * tfe . RunVariable , 0 , len ( variables ) )
2021-08-23 12:35:54 -05:00
for name , value := range variables {
runVariables = append ( runVariables , & tfe . RunVariable {
Key : name ,
Value : value ,
} )
}
runOptions . Variables = runVariables
2023-05-30 02:17:02 -05:00
if len ( op . GenerateConfigOut ) > 0 {
runOptions . AllowConfigGeneration = tfe . Bool ( true )
}
2021-08-12 14:30:24 -05:00
r , err := b . client . Runs . Create ( stopCtx , runOptions )
if err != nil {
return r , generalError ( "Failed to create run" , err )
}
// When the lock timeout is set, if the run is still pending and
// cancellable after that period, we attempt to cancel it.
if lockTimeout := op . StateLocker . Timeout ( ) ; lockTimeout > 0 {
go func ( ) {
select {
case <- stopCtx . Done ( ) :
return
case <- cancelCtx . Done ( ) :
return
case <- time . After ( lockTimeout ) :
// Retrieve the run to get its current status.
r , err := b . client . Runs . Read ( cancelCtx , r . ID )
if err != nil {
log . Printf ( "[ERROR] error reading run: %v" , err )
return
}
if r . Status == tfe . RunPending && r . Actions . IsCancelable {
if b . CLI != nil {
b . CLI . Output ( b . Colorize ( ) . Color ( strings . TrimSpace ( lockTimeoutErr ) ) )
}
// We abuse the auto aprove flag to indicate that we do not
// want to ask if the remote operation should be canceled.
op . AutoApprove = true
p , err := os . FindProcess ( os . Getpid ( ) )
if err != nil {
log . Printf ( "[ERROR] error searching process ID: %v" , err )
return
}
p . Signal ( syscall . SIGINT )
}
}
} ( )
}
if b . CLI != nil {
b . CLI . Output ( b . Colorize ( ) . Color ( strings . TrimSpace ( fmt . Sprintf (
runHeader , b . hostname , b . organization , op . Workspace , r . ID ) ) + "\n" ) )
}
2023-04-17 10:53:47 -05:00
// Render any warnings that were raised during run creation
if err := b . renderRunWarnings ( stopCtx , b . client , r . ID ) ; err != nil {
return r , err
}
2022-07-19 22:07:25 -05:00
// Retrieve the run to get task stages.
// Task Stages are calculated upfront so we only need to call this once for the run.
2022-09-20 03:08:10 -05:00
taskStages , err := b . runTaskStages ( stopCtx , b . client , r . ID )
if err != nil {
return r , err
2022-07-19 22:07:25 -05:00
}
2022-09-20 03:08:10 -05:00
if stage , ok := taskStages [ tfe . PrePlan ] ; ok {
if err := b . waitTaskStage ( stopCtx , cancelCtx , op , r , stage . ID , "Pre-plan Tasks" ) ; err != nil {
2022-07-19 22:07:25 -05:00
return r , err
}
}
2021-08-12 14:30:24 -05:00
r , err = b . waitForRun ( stopCtx , cancelCtx , op , "plan" , r , w )
if err != nil {
return r , err
}
2023-02-09 06:35:48 -06:00
err = b . renderPlanLogs ( stopCtx , op , r )
2021-08-12 14:30:24 -05:00
if err != nil {
2023-02-09 06:35:48 -06:00
return r , err
2021-08-12 14:30:24 -05:00
}
// Retrieve the run to get its current status.
2022-07-19 22:07:25 -05:00
r , err = b . client . Runs . Read ( stopCtx , r . ID )
2021-08-12 14:30:24 -05:00
if err != nil {
2022-07-19 22:07:25 -05:00
return r , generalError ( "Failed to retrieve run" , err )
2021-08-12 14:30:24 -05:00
}
// If the run is canceled or errored, we still continue to the
// cost-estimation and policy check phases to ensure we render any
// results available. In the case of a hard-failed policy check, the
// status of the run will be "errored", but there is still policy
// information which should be shown.
2022-09-20 03:08:10 -05:00
if stage , ok := taskStages [ tfe . PostPlan ] ; ok {
if err := b . waitTaskStage ( stopCtx , cancelCtx , op , r , stage . ID , "Post-plan Tasks" ) ; err != nil {
2022-02-01 15:41:29 -06:00
return r , err
}
}
2021-08-12 14:30:24 -05:00
// Show any cost estimation output.
if r . CostEstimate != nil {
err = b . costEstimate ( stopCtx , cancelCtx , op , r )
if err != nil {
return r , err
}
}
// Check any configured sentinel policies.
if len ( r . PolicyChecks ) > 0 {
err = b . checkPolicy ( stopCtx , cancelCtx , op , r )
if err != nil {
return r , err
}
}
return r , nil
}
2023-05-31 14:55:35 -05:00
// AssertImportCompatible errors if the user is attempting to use configuration-
// driven import and the version of the agent or API is too low to support it.
func ( b * Cloud ) AssertImportCompatible ( config * configs . Config ) error {
// Check TFC_RUN_ID is populated, indicating we are running in a remote TFC
// execution environment.
if len ( config . Module . Import ) > 0 && os . Getenv ( "TFC_RUN_ID" ) != "" {
// First, check the remote API version is high enough.
currentAPIVersion , err := version . NewVersion ( b . client . RemoteAPIVersion ( ) )
if err != nil {
2023-08-22 09:31:53 -05:00
return fmt . Errorf ( "Error parsing remote API version. To proceed, please remove any import blocks from your config. Please report the following error to the OpenTF team: %s" , err )
2023-05-31 14:55:35 -05:00
}
desiredAPIVersion , _ := version . NewVersion ( "2.6" )
if currentAPIVersion . LessThan ( desiredAPIVersion ) {
2023-08-23 11:04:21 -05:00
return fmt . Errorf ( "Import blocks are not supported in this version of the cloud backend. Please remove any import blocks from your config or upgrade the cloud backend." )
2023-05-31 14:55:35 -05:00
}
// Second, check the agent version is high enough.
agentEnv , isSet := os . LookupEnv ( "TFC_AGENT_VERSION" )
if ! isSet {
2023-08-22 09:31:53 -05:00
return fmt . Errorf ( "Error reading TFC agent version. To proceed, please remove any import blocks from your config. Please report the following error to the OpenTF team: TFC_AGENT_VERSION not present." )
2023-05-31 14:55:35 -05:00
}
currentAgentVersion , err := version . NewVersion ( agentEnv )
if err != nil {
2023-08-22 09:31:53 -05:00
return fmt . Errorf ( "Error parsing TFC agent version. To proceed, please remove any import blocks from your config. Please report the following error to the OpenTF team: %s" , err )
2023-05-31 14:55:35 -05:00
}
desiredAgentVersion , _ := version . NewVersion ( "1.10" )
if currentAgentVersion . LessThan ( desiredAgentVersion ) {
return fmt . Errorf ( "Import blocks are not supported in this version of the Terraform Cloud Agent. You are using agent version %s, but this feature requires version %s. Please remove any import blocks from your config or upgrade your agent." , currentAgentVersion , desiredAgentVersion )
}
}
return nil
}
2023-02-09 06:35:48 -06:00
// renderPlanLogs reads the streamed plan JSON logs and calls the JSON Plan renderer (jsonformat.RenderPlan) to
// render the plan output. The plan output is fetched from the redacted output endpoint.
func ( b * Cloud ) renderPlanLogs ( ctx context . Context , op * backend . Operation , run * tfe . Run ) error {
logs , err := b . client . Plans . Logs ( ctx , run . Plan . ID )
if err != nil {
return err
}
if b . CLI != nil {
reader := bufio . NewReaderSize ( logs , 64 * 1024 )
for next := true ; next ; {
var l , line [ ] byte
var err error
for isPrefix := true ; isPrefix ; {
l , isPrefix , err = reader . ReadLine ( )
if err != nil {
if err != io . EOF {
return generalError ( "Failed to read logs" , err )
}
next = false
}
line = append ( line , l ... )
}
if next || len ( line ) > 0 {
log := & jsonformat . JSONLog { }
if err := json . Unmarshal ( line , log ) ; err != nil {
// If we can not parse the line as JSON, we will simply
// print the line. This maintains backwards compatibility for
// users who do not wish to enable structured output in their
// workspace.
b . CLI . Output ( string ( line ) )
continue
}
// We will ignore plan output, change summary or outputs logs
// during the plan phase.
if log . Type == jsonformat . LogOutputs ||
log . Type == jsonformat . LogChangeSummary ||
log . Type == jsonformat . LogPlannedChange {
continue
}
if b . renderer != nil {
// Otherwise, we will print the log
err := b . renderer . RenderLog ( log )
if err != nil {
return err
}
}
}
}
}
2023-05-30 02:17:02 -05:00
// Get the run's current status and include the workspace and plan. We will check if
// the run has errored, if structured output is enabled, and if the plan
2023-02-09 06:35:48 -06:00
run , err = b . client . Runs . ReadWithOptions ( ctx , run . ID , & tfe . RunReadOptions {
2023-05-30 02:17:02 -05:00
Include : [ ] tfe . RunIncludeOpt { tfe . RunWorkspace , tfe . RunPlan } ,
2023-02-09 06:35:48 -06:00
} )
if err != nil {
return err
}
// If the run was errored, canceled, or discarded we will not resume the rest
2023-05-30 02:17:02 -05:00
// of this logic and attempt to render the plan, except in certain special circumstances
// where the plan errored but successfully generated configuration during an
// import operation. In that case, we need to keep going so we can load the JSON plan
// and use it to write the generated config to the specified output file.
shouldGenerateConfig := shouldGenerateConfig ( op . GenerateConfigOut , run )
shouldRenderPlan := shouldRenderPlan ( run )
if ! shouldRenderPlan && ! shouldGenerateConfig {
2023-02-09 06:35:48 -06:00
// We won't return an error here since we need to resume the logic that
// follows after rendering the logs (run tasks, cost estimation, etc.)
return nil
}
2023-05-30 02:17:02 -05:00
// Fetch the redacted JSON plan if we need it for either rendering the plan
// or writing out generated configuration.
var redactedPlan * jsonformat . Plan
renderSRO , err := b . shouldRenderStructuredRunOutput ( run )
if err != nil {
return err
}
if renderSRO || shouldGenerateConfig {
2023-06-26 19:41:24 -05:00
jsonBytes , err := readRedactedPlan ( ctx , b . client . BaseURL ( ) , b . token , run . Plan . ID )
2023-02-09 06:35:48 -06:00
if err != nil {
2023-05-30 02:17:02 -05:00
return generalError ( "Failed to read JSON plan" , err )
2023-02-09 06:35:48 -06:00
}
2023-06-26 19:41:24 -05:00
redactedPlan , err = decodeRedactedPlan ( jsonBytes )
if err != nil {
return generalError ( "Failed to decode JSON plan" , err )
}
2023-05-30 02:17:02 -05:00
}
2023-02-09 06:35:48 -06:00
2023-05-30 02:17:02 -05:00
// Write any generated config before rendering the plan, so we can stop in case of errors
if shouldGenerateConfig {
diags := maybeWriteGeneratedConfig ( redactedPlan , op . GenerateConfigOut )
if diags . HasErrors ( ) {
return diags . Err ( )
}
}
// Only generate the human readable output from the plan if structured run output is
// enabled. Otherwise we risk duplicate plan output since plan output may also be
// shown in the streamed logs.
if shouldRenderPlan && renderSRO {
b . renderer . RenderHumanPlan ( * redactedPlan , op . PlanMode )
2023-02-09 06:35:48 -06:00
}
return nil
}
2023-05-30 02:17:02 -05:00
// maybeWriteGeneratedConfig attempts to write any generated configuration from the JSON plan
// to the specified output file, if generated configuration exists and the correct flag was
// passed to the plan command.
func maybeWriteGeneratedConfig ( plan * jsonformat . Plan , out string ) ( diags tfdiags . Diagnostics ) {
if genconfig . ShouldWriteConfig ( out ) {
diags := genconfig . ValidateTargetFile ( out )
if diags . HasErrors ( ) {
return diags
}
var writer io . Writer
for _ , c := range plan . ResourceChanges {
change := genconfig . Change {
Addr : c . Address ,
GeneratedConfig : c . Change . GeneratedConfig ,
}
if c . Change . Importing != nil {
change . ImportID = c . Change . Importing . ID
}
var moreDiags tfdiags . Diagnostics
writer , _ , moreDiags = change . MaybeWriteConfig ( writer , out )
if moreDiags . HasErrors ( ) {
return diags . Append ( moreDiags )
}
}
}
return diags
}
2023-04-11 15:47:56 -05:00
// shouldRenderStructuredRunOutput ensures the remote workspace has structured
// run output enabled and, if using Terraform Enterprise, ensures it is a release
// that supports enabling SRO for CLI-driven runs. The plan output will have
// already been rendered when the logs were read if this wasn't the case.
func ( b * Cloud ) shouldRenderStructuredRunOutput ( run * tfe . Run ) ( bool , error ) {
if b . renderer == nil || ! run . Workspace . StructuredRunOutputEnabled {
return false , nil
}
// If the cloud backend is configured against TFC, we only require that
// the workspace has structured run output enabled.
if b . client . IsCloud ( ) && run . Workspace . StructuredRunOutputEnabled {
return true , nil
}
// If the cloud backend is configured against TFE, ensure the release version
// supports enabling SRO for CLI runs.
if b . client . IsEnterprise ( ) {
tfeVersion := b . client . RemoteTFEVersion ( )
if tfeVersion != "" {
v := strings . Split ( tfeVersion [ 1 : ] , "-" )
releaseDate , err := strconv . Atoi ( v [ 0 ] )
if err != nil {
return false , err
}
// Any release older than 202302-1 will not support enabling SRO for
// CLI-driven runs
if releaseDate < 202302 {
return false , nil
} else if run . Workspace . StructuredRunOutputEnabled {
return true , nil
}
}
}
// Version of TFE is unknowable
return false , nil
}
2023-05-30 02:17:02 -05:00
func shouldRenderPlan ( run * tfe . Run ) bool {
return ! ( run . Status == tfe . RunErrored || run . Status == tfe . RunCanceled ||
run . Status == tfe . RunDiscarded )
}
func shouldGenerateConfig ( out string , run * tfe . Run ) bool {
return ( run . Plan . Status == tfe . PlanErrored || run . Plan . Status == tfe . PlanFinished ) &&
2023-05-30 20:00:41 -05:00
run . Plan . GeneratedConfiguration && len ( out ) > 0
2023-05-30 02:17:02 -05:00
}
2021-08-12 14:30:24 -05:00
const planDefaultHeader = `
[ reset ] [ yellow ] Running plan in Terraform Cloud . Output will stream here . Pressing Ctrl - C
will stop streaming the logs , but will not stop the plan running remotely . [ reset ]
Preparing the remote plan ...
`
const runHeader = `
[ reset ] [ yellow ] To view this run in a browser , visit :
https : //%s/app/%s/%s/runs/%s[reset]
`
// The newline in this error is to make it look good in the CLI!
const lockTimeoutErr = `
[ reset ] [ red ] Lock timeout exceeded , sending interrupt to cancel the remote operation .
[ reset ]
`