github.com/opentofu/opentofu@v1.7.1/internal/command/views/show.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package views 7 8 import ( 9 "bytes" 10 "encoding/json" 11 "fmt" 12 13 "github.com/opentofu/opentofu/internal/cloud/cloudplan" 14 "github.com/opentofu/opentofu/internal/command/arguments" 15 "github.com/opentofu/opentofu/internal/command/jsonformat" 16 "github.com/opentofu/opentofu/internal/command/jsonplan" 17 "github.com/opentofu/opentofu/internal/command/jsonprovider" 18 "github.com/opentofu/opentofu/internal/command/jsonstate" 19 "github.com/opentofu/opentofu/internal/configs" 20 "github.com/opentofu/opentofu/internal/plans" 21 "github.com/opentofu/opentofu/internal/states/statefile" 22 "github.com/opentofu/opentofu/internal/tfdiags" 23 "github.com/opentofu/opentofu/internal/tofu" 24 ) 25 26 type Show interface { 27 // Display renders the plan, if it is available. If plan is nil, it renders the statefile. 28 Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *tofu.Schemas) int 29 30 // Diagnostics renders early diagnostics, resulting from argument parsing. 31 Diagnostics(diags tfdiags.Diagnostics) 32 } 33 34 func NewShow(vt arguments.ViewType, view *View) Show { 35 switch vt { 36 case arguments.ViewJSON: 37 return &ShowJSON{view: view} 38 case arguments.ViewHuman: 39 return &ShowHuman{view: view} 40 default: 41 panic(fmt.Sprintf("unknown view type %v", vt)) 42 } 43 } 44 45 type ShowHuman struct { 46 view *View 47 } 48 49 var _ Show = (*ShowHuman)(nil) 50 51 func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *tofu.Schemas) int { 52 renderer := jsonformat.Renderer{ 53 Colorize: v.view.colorize, 54 Streams: v.view.streams, 55 RunningInAutomation: v.view.runningInAutomation, 56 } 57 58 // Prefer to display a pre-built JSON plan, if we got one; then, fall back 59 // to building one ourselves. 60 if planJSON != nil { 61 if !planJSON.Redacted { 62 v.view.streams.Eprintf("Didn't get renderable JSON plan format for human display") 63 return 1 64 } 65 // The redacted json plan format can be decoded into a jsonformat.Plan 66 p := jsonformat.Plan{} 67 r := bytes.NewReader(planJSON.JSONBytes) 68 if err := json.NewDecoder(r).Decode(&p); err != nil { 69 v.view.streams.Eprintf("Couldn't decode renderable JSON plan format: %s", err) 70 } 71 72 v.view.streams.Print(v.view.colorize.Color(planJSON.RunHeader + "\n")) 73 renderer.RenderHumanPlan(p, planJSON.Mode, planJSON.Qualities...) 74 v.view.streams.Print(v.view.colorize.Color("\n" + planJSON.RunFooter + "\n")) 75 } else if plan != nil { 76 outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas) 77 if err != nil { 78 v.view.streams.Eprintf("Failed to marshal plan to json: %s", err) 79 return 1 80 } 81 82 jplan := jsonformat.Plan{ 83 PlanFormatVersion: jsonplan.FormatVersion, 84 ProviderFormatVersion: jsonprovider.FormatVersion, 85 OutputChanges: outputs, 86 ResourceChanges: changed, 87 ResourceDrift: drift, 88 ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), 89 RelevantAttributes: attrs, 90 } 91 92 var opts []plans.Quality 93 if !plan.CanApply() { 94 opts = append(opts, plans.NoChanges) 95 } 96 if plan.Errored { 97 opts = append(opts, plans.Errored) 98 } 99 100 renderer.RenderHumanPlan(jplan, plan.UIMode, opts...) 101 } else { 102 if stateFile == nil { 103 v.view.streams.Println("No state.") 104 return 0 105 } 106 107 root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas) 108 if err != nil { 109 v.view.streams.Eprintf("Failed to marshal state to json: %s", err) 110 return 1 111 } 112 113 jstate := jsonformat.State{ 114 StateFormatVersion: jsonstate.FormatVersion, 115 ProviderFormatVersion: jsonprovider.FormatVersion, 116 RootModule: root, 117 RootModuleOutputs: outputs, 118 ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), 119 } 120 121 renderer.RenderHumanState(jstate) 122 } 123 return 0 124 } 125 126 func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) { 127 v.view.Diagnostics(diags) 128 } 129 130 type ShowJSON struct { 131 view *View 132 } 133 134 var _ Show = (*ShowJSON)(nil) 135 136 func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *tofu.Schemas) int { 137 // Prefer to display a pre-built JSON plan, if we got one; then, fall back 138 // to building one ourselves. 139 if planJSON != nil { 140 if planJSON.Redacted { 141 v.view.streams.Eprintf("Didn't get external JSON plan format") 142 return 1 143 } 144 v.view.streams.Println(string(planJSON.JSONBytes)) 145 } else if plan != nil { 146 planJSON, err := jsonplan.Marshal(config, plan, stateFile, schemas) 147 148 if err != nil { 149 v.view.streams.Eprintf("Failed to marshal plan to json: %s", err) 150 return 1 151 } 152 v.view.streams.Println(string(planJSON)) 153 } else { 154 // It is possible that there is neither state nor a plan. 155 // That's ok, we'll just return an empty object. 156 jsonState, err := jsonstate.Marshal(stateFile, schemas) 157 if err != nil { 158 v.view.streams.Eprintf("Failed to marshal state to json: %s", err) 159 return 1 160 } 161 v.view.streams.Println(string(jsonState)) 162 } 163 return 0 164 } 165 166 // Diagnostics should only be called if show cannot be executed. 167 // In this case, we choose to render human-readable diagnostic output, 168 // primarily for backwards compatibility. 169 func (v *ShowJSON) Diagnostics(diags tfdiags.Diagnostics) { 170 v.view.Diagnostics(diags) 171 }