opentofu/internal/cloud/backend_show.go
Marcin Białoń 4ecb07bf8f
update references
Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
2023-08-22 16:31:53 +02:00

115 lines
3.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package cloud
import (
"context"
"fmt"
"strings"
tfe "github.com/hashicorp/go-tfe"
"github.com/placeholderplaceholderplaceholder/opentf/internal/cloud/cloudplan"
"github.com/placeholderplaceholderplaceholder/opentf/internal/plans"
)
// ShowPlanForRun downloads the JSON plan output for the specified cloud run
// (either the redacted or unredacted format, per the caller's request), and
// returns it in a cloudplan.RemotePlanJSON wrapper struct (along with various
// metadata required by terraform show). It's intended for use by the terraform
// show command, in order to format and display a saved cloud plan.
func (b *Cloud) ShowPlanForRun(ctx context.Context, runID, runHostname string, redacted bool) (*cloudplan.RemotePlanJSON, error) {
var jsonBytes []byte
mode := plans.NormalMode
var opts []plans.Quality
// Bail early if wrong hostname
if runHostname != b.hostname {
return nil, fmt.Errorf("hostname for run (%s) does not match the configured cloud integration (%s)", runHostname, b.hostname)
}
// Get run and plan
r, err := b.client.Runs.ReadWithOptions(ctx, runID, &tfe.RunReadOptions{Include: []tfe.RunIncludeOpt{tfe.RunPlan, tfe.RunWorkspace}})
if err == tfe.ErrResourceNotFound {
return nil, fmt.Errorf("couldn't read information for cloud run %s; make sure you've run `opentf login` and that you have permission to view the run", runID)
} else if err != nil {
return nil, fmt.Errorf("couldn't read information for cloud run %s: %w", runID, err)
}
// Sort out the run mode
if r.IsDestroy {
mode = plans.DestroyMode
} else if r.RefreshOnly {
mode = plans.RefreshOnlyMode
}
// Check that the plan actually finished
switch r.Plan.Status {
case tfe.PlanErrored:
// Errored plans might still be displayable, but we want to mention it to the renderer.
opts = append(opts, plans.Errored)
case tfe.PlanFinished:
// Good to go, but alert the renderer if it has no changes.
if !r.Plan.HasChanges {
opts = append(opts, plans.NoChanges)
}
default:
// Bail, we can't use this.
err = fmt.Errorf("can't display a cloud plan that is currently %s", r.Plan.Status)
return nil, err
}
// Fetch the json plan!
if redacted {
jsonBytes, err = readRedactedPlan(ctx, b.client.BaseURL(), b.token, r.Plan.ID)
} else {
jsonBytes, err = b.client.Plans.ReadJSONOutput(ctx, r.Plan.ID)
}
if err == tfe.ErrResourceNotFound {
if redacted {
return nil, fmt.Errorf("couldn't read plan data for cloud run %s; make sure you've run `opentf login` and that you have permission to view the run", runID)
} else {
return nil, fmt.Errorf("couldn't read unredacted JSON plan data for cloud run %s; make sure you've run `opentf login` and that you have admin permissions on the workspace", runID)
}
} else if err != nil {
return nil, fmt.Errorf("couldn't read plan data for cloud run %s: %w", runID, err)
}
// Format a run header and footer
header := strings.TrimSpace(fmt.Sprintf(runHeader, b.hostname, b.organization, r.Workspace.Name, r.ID))
footer := strings.TrimSpace(statusFooter(r.Status, r.Actions.IsConfirmable, r.Workspace.Locked))
out := &cloudplan.RemotePlanJSON{
JSONBytes: jsonBytes,
Redacted: redacted,
Mode: mode,
Qualities: opts,
RunHeader: header,
RunFooter: footer,
}
return out, nil
}
func statusFooter(status tfe.RunStatus, isConfirmable, locked bool) string {
statusText := strings.ReplaceAll(string(status), "_", " ")
statusColor := "red"
statusNote := "not confirmable"
if isConfirmable {
statusColor = "green"
statusNote = "confirmable"
}
lockedColor := "green"
lockedText := "unlocked"
if locked {
lockedColor = "red"
lockedText = "locked"
}
return fmt.Sprintf(statusFooterText, statusColor, statusText, statusNote, lockedColor, lockedText)
}
const statusFooterText = `
[reset][%s]Run status: %s (%s)[reset]
[%s]Workspace is %s[reset]
`