mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-21 02:37:43 -05:00
We previously added the -config mode for showing the entire assembled configuration tree, including the content of any descendent modules, but that mode requires first running "tofu init" to install all of the provider and module dependencies of the configuration. This new -module=DIR mode returns a subset of the same JSON representation for only a single module that can be generated without first installing any dependencies, making this mode more appropriate for situations like generating documentation for a single module when importing it into the OpenTofu Registry. The registry generation process does not want to endure the overhead of installing other providers and modules when all it actually needs is metadata about the top-level declarations in the module. To minimize the risk to the already-working full-config JSON representation while still reusing most of its code, the implementation details of package jsonconfig are a little awkward here. Since this code changes relatively infrequently and is implementing an external interface subject to compatibility constraints, and since this new behavior is relatively marginal and intended primarily for our own OpenTofu Registry purposes, this is a pragmatic tradeoff that is hopefully compensated for well enough by the code comments that aim to explain what's going on for the benefit of future maintainers. If we _do_ find ourselves making substantial changes to this code at a later date then we can consider a more significant restructure of the code at that point; the weird stuff is intentionally encapsulated inside package jsonconfig so it can change later without changing any callers. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
245 lines
8.1 KiB
Go
245 lines
8.1 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package views
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/opentofu/opentofu/internal/cloud/cloudplan"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
"github.com/opentofu/opentofu/internal/command/jsonconfig"
|
|
"github.com/opentofu/opentofu/internal/command/jsonformat"
|
|
"github.com/opentofu/opentofu/internal/command/jsonplan"
|
|
"github.com/opentofu/opentofu/internal/command/jsonprovider"
|
|
"github.com/opentofu/opentofu/internal/command/jsonstate"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
)
|
|
|
|
type Show interface {
|
|
// DisplayState renders the given state snapshot, returning a status code for "tofu show" to return.
|
|
DisplayState(ctx context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int
|
|
|
|
// DisplayPlan renders the given plan, returning a status code for "tofu show" to return.
|
|
//
|
|
// Unfortunately there are two possible ways to represent a plan:
|
|
// - Locally-generated plans are loaded as *plans.Plan.
|
|
// - Remotely-generated plans (using remote operations) are loaded as *cloudplan.RemotePlanJSON.
|
|
//
|
|
// Therefore the implementation of this method must handle both cases,
|
|
// preferring planJSON if it is not nil and using plan otherwise.
|
|
DisplayPlan(ctx context.Context, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, config *configs.Config, priorStateFile *statefile.File, schemas *tofu.Schemas) int
|
|
|
|
// DisplayConfig renders the given configuration, returning a status code for "tofu show" to return.
|
|
DisplayConfig(config *configs.Config, schemas *tofu.Schemas) int
|
|
|
|
// DisplaySingleModule renders just one module, in a format that's a subset
|
|
// of that used by [Show.DisplayConfig] which we can produce without
|
|
// schema or child module information.
|
|
DisplaySingleModule(module *configs.Module) int
|
|
|
|
// Diagnostics renders early diagnostics, resulting from argument parsing.
|
|
Diagnostics(diags tfdiags.Diagnostics)
|
|
}
|
|
|
|
func NewShow(vt arguments.ViewType, view *View) Show {
|
|
switch vt {
|
|
case arguments.ViewJSON:
|
|
return &ShowJSON{view: view}
|
|
case arguments.ViewHuman:
|
|
return &ShowHuman{view: view}
|
|
default:
|
|
panic(fmt.Sprintf("unknown view type %v", vt))
|
|
}
|
|
}
|
|
|
|
type ShowHuman struct {
|
|
view *View
|
|
}
|
|
|
|
var _ Show = (*ShowHuman)(nil)
|
|
|
|
func (v *ShowHuman) DisplayState(_ context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int {
|
|
renderer := jsonformat.Renderer{
|
|
Colorize: v.view.colorize,
|
|
Streams: v.view.streams,
|
|
RunningInAutomation: v.view.runningInAutomation,
|
|
ShowSensitive: v.view.showSensitive,
|
|
}
|
|
|
|
if stateFile == nil {
|
|
v.view.streams.Println("No state.")
|
|
return 0
|
|
}
|
|
|
|
root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas)
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
|
|
return 1
|
|
}
|
|
|
|
jstate := jsonformat.State{
|
|
StateFormatVersion: jsonstate.FormatVersion,
|
|
ProviderFormatVersion: jsonprovider.FormatVersion,
|
|
RootModule: root,
|
|
RootModuleOutputs: outputs,
|
|
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
|
|
}
|
|
|
|
renderer.RenderHumanState(jstate)
|
|
return 0
|
|
}
|
|
|
|
func (v *ShowHuman) DisplayPlan(_ context.Context, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, config *configs.Config, priorStateFile *statefile.File, schemas *tofu.Schemas) int {
|
|
renderer := jsonformat.Renderer{
|
|
Colorize: v.view.colorize,
|
|
Streams: v.view.streams,
|
|
RunningInAutomation: v.view.runningInAutomation,
|
|
ShowSensitive: v.view.showSensitive,
|
|
}
|
|
|
|
// Prefer to display a pre-built JSON plan, if we got one; then, fall back
|
|
// to building one ourselves.
|
|
if planJSON != nil {
|
|
if !planJSON.Redacted {
|
|
v.view.streams.Eprintf("Didn't get renderable JSON plan format for human display")
|
|
return 1
|
|
}
|
|
// The redacted json plan format can be decoded into a jsonformat.Plan
|
|
p := jsonformat.Plan{}
|
|
r := bytes.NewReader(planJSON.JSONBytes)
|
|
if err := json.NewDecoder(r).Decode(&p); err != nil {
|
|
v.view.streams.Eprintf("Couldn't decode renderable JSON plan format: %s", err)
|
|
}
|
|
|
|
v.view.streams.Print(v.view.colorize.Color(planJSON.RunHeader + "\n"))
|
|
renderer.RenderHumanPlan(p, planJSON.Mode, planJSON.Qualities...)
|
|
v.view.streams.Print(v.view.colorize.Color("\n" + planJSON.RunFooter + "\n"))
|
|
} else if plan != nil {
|
|
outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas)
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
|
|
return 1
|
|
}
|
|
|
|
jplan := jsonformat.Plan{
|
|
PlanFormatVersion: jsonplan.FormatVersion,
|
|
ProviderFormatVersion: jsonprovider.FormatVersion,
|
|
OutputChanges: outputs,
|
|
ResourceChanges: changed,
|
|
ResourceDrift: drift,
|
|
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
|
|
RelevantAttributes: attrs,
|
|
}
|
|
|
|
var opts []plans.Quality
|
|
if !plan.CanApply() {
|
|
opts = append(opts, plans.NoChanges)
|
|
}
|
|
if plan.Errored {
|
|
opts = append(opts, plans.Errored)
|
|
}
|
|
|
|
renderer.RenderHumanPlan(jplan, plan.UIMode, opts...)
|
|
} else {
|
|
v.view.streams.Println("No plan.")
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (v *ShowHuman) DisplayConfig(config *configs.Config, schemas *tofu.Schemas) int {
|
|
// The human view should never be called for configuration display
|
|
// since we require -json for -config
|
|
v.view.streams.Eprintf("Internal error: human view should not be used for configuration display")
|
|
return 1
|
|
}
|
|
|
|
func (v *ShowHuman) DisplaySingleModule(_ *configs.Module) int {
|
|
// The human view should never be called for module display
|
|
// since we require -json for -module=DIR.
|
|
v.view.streams.Eprintf("Internal error: human view should not be used for module display")
|
|
return 1
|
|
}
|
|
|
|
func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
|
v.view.Diagnostics(diags)
|
|
}
|
|
|
|
type ShowJSON struct {
|
|
view *View
|
|
}
|
|
|
|
var _ Show = (*ShowJSON)(nil)
|
|
|
|
func (v *ShowJSON) DisplayState(_ context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int {
|
|
jsonState, err := jsonstate.Marshal(stateFile, schemas)
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
|
|
return 1
|
|
}
|
|
v.view.streams.Println(string(jsonState))
|
|
return 0
|
|
}
|
|
|
|
func (v *ShowJSON) DisplayPlan(_ context.Context, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, config *configs.Config, priorStateFile *statefile.File, schemas *tofu.Schemas) int {
|
|
// Prefer to display a pre-built JSON plan, if we got one; then, fall back
|
|
// to building one ourselves.
|
|
if planJSON != nil {
|
|
if planJSON.Redacted {
|
|
v.view.streams.Eprintf("Didn't get external JSON plan format")
|
|
return 1
|
|
}
|
|
v.view.streams.Println(string(planJSON.JSONBytes))
|
|
} else if plan != nil {
|
|
planJSON, err := jsonplan.Marshal(config, plan, priorStateFile, schemas)
|
|
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
|
|
return 1
|
|
}
|
|
v.view.streams.Println(string(planJSON))
|
|
} else {
|
|
// Should not get here because at least one of the two plan arguments
|
|
// should be present, but we'll tolerate this by just returning an
|
|
// empty JSON object.
|
|
v.view.streams.Println("{}")
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (v *ShowJSON) DisplayConfig(config *configs.Config, schemas *tofu.Schemas) int {
|
|
configJSON, err := jsonconfig.Marshal(config, schemas)
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal configuration to JSON: %s", err)
|
|
return 1
|
|
}
|
|
v.view.streams.Println(string(configJSON))
|
|
return 0
|
|
}
|
|
|
|
func (v *ShowJSON) DisplaySingleModule(module *configs.Module) int {
|
|
moduleJSON, err := jsonconfig.MarshalSingleModule(module)
|
|
if err != nil {
|
|
v.view.streams.Eprintf("Failed to marshal module contents to JSON: %s", err)
|
|
return 1
|
|
}
|
|
v.view.streams.Println(string(moduleJSON))
|
|
return 0
|
|
}
|
|
|
|
// Diagnostics should only be called if show cannot be executed.
|
|
// In this case, we choose to render human-readable diagnostic output,
|
|
// primarily for backwards compatibility.
|
|
func (v *ShowJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
|
v.view.Diagnostics(diags)
|
|
}
|