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