mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-01 11:47:07 -06:00
4ecb07bf8f
Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
115 lines
3.8 KiB
Go
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]
|
|
`
|