github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/model/state_initialize.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package model 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "deps.dev/util/resolve" 23 "github.com/charmbracelet/bubbles/spinner" 24 tea "github.com/charmbracelet/bubbletea" 25 "github.com/google/osv-scalibr/guidedremediation/internal/lockfile" 26 "github.com/google/osv-scalibr/guidedremediation/internal/manifest" 27 "github.com/google/osv-scalibr/guidedremediation/internal/parser" 28 "github.com/google/osv-scalibr/guidedremediation/internal/remediation" 29 "github.com/google/osv-scalibr/guidedremediation/internal/strategy/inplace" 30 "github.com/google/osv-scalibr/guidedremediation/internal/tui/components" 31 "github.com/google/osv-scalibr/guidedremediation/options" 32 "github.com/google/osv-scalibr/guidedremediation/result" 33 ) 34 35 type stateInitialize struct { 36 spinner spinner.Model 37 } 38 39 func newStateInitialize() stateInitialize { 40 return stateInitialize{ 41 spinner: components.NewSpinner(), 42 } 43 } 44 45 func (s stateInitialize) Init(m Model) tea.Cmd { 46 cmds := []tea.Cmd{s.spinner.Tick} 47 if m.options.Lockfile != "" { 48 // if we have a lockfile, start calculating the in-place updates 49 cmds = append(cmds, doInPlaceResolutionCmd(m.options, m.lockfileRW)) 50 } else { 51 // if we don't have a lockfile, start calculating the relock result 52 cmds = append(cmds, doInitialRelockCmd(m.options, m.manifestRW)) 53 } 54 55 return tea.Batch(cmds...) 56 } 57 58 func (s stateInitialize) Update(m Model, msg tea.Msg) (tea.Model, tea.Cmd) { 59 var c tea.Cmd 60 s.spinner, c = s.spinner.Update(msg) 61 m.st = s 62 cmds := []tea.Cmd{c} 63 switch msg := msg.(type) { 64 // in-place resolution finished 65 case inPlaceResolutionMsg: 66 if msg.err != nil { 67 return errorAndExit(m, msg.err) 68 } 69 // set the result and start the relock computation 70 m.lockfileGraph = msg.resolvedGraph 71 m.lockfilePatches = msg.allPatches 72 if m.options.Manifest != "" { 73 cmds = append(cmds, doInitialRelockCmd(m.options, m.manifestRW)) 74 } else { 75 m.st = newStateChooseStrategy(m) 76 cmds = append(cmds, m.st.Init(m)) 77 } 78 79 // relocking finished 80 case doRelockMsg: 81 if msg.err != nil { 82 return errorAndExit(m, msg.err) 83 } 84 // set the result and go to next state 85 m.relockBaseManifest = msg.resolvedManifest 86 m.relockBaseErrors = computeResolveErrors(msg.resolvedManifest.Graph) 87 if m.options.Lockfile == "" { 88 m.st = stateRelockResult{} 89 } else { 90 m.st = newStateChooseStrategy(m) 91 } 92 cmds = append(cmds, m.st.Init(m)) 93 } 94 95 return m, tea.Batch(cmds...) 96 } 97 98 func (s stateInitialize) View(m Model) string { 99 sb := strings.Builder{} 100 if m.options.Lockfile == "" { 101 sb.WriteString("No lockfile provided. Assuming re-lock.\n") 102 } else { 103 sb.WriteString(fmt.Sprintf("Scanning %s ", components.SelectedTextStyle.Render(m.options.Lockfile))) 104 if m.lockfileGraph.Graph == nil { 105 sb.WriteString(s.spinner.View()) 106 sb.WriteString("\n") 107 108 return sb.String() 109 } 110 sb.WriteString("✓\n") 111 } 112 113 sb.WriteString(fmt.Sprintf("Resolving %s ", components.SelectedTextStyle.Render(m.options.Manifest))) 114 if m.relockBaseManifest == nil { 115 sb.WriteString(s.spinner.View()) 116 sb.WriteString("\n") 117 } else { 118 sb.WriteString("✓\n") 119 } 120 121 return sb.String() 122 } 123 124 func (s stateInitialize) InfoView() string { return "" } 125 func (s stateInitialize) Resize(_, _ int) modelState { return s } 126 func (s stateInitialize) ResizeInfo(_, _ int) modelState { return s } 127 func (s stateInitialize) IsInfoFocused() bool { return false } 128 129 type inPlaceResolutionMsg struct { 130 resolvedGraph remediation.ResolvedGraph 131 allPatches []result.Patch 132 err error 133 } 134 135 func doInPlaceResolutionCmd(opts options.FixVulnsOptions, rw lockfile.ReadWriter) tea.Cmd { 136 return func() tea.Msg { 137 g, err := parser.ParseLockfile(opts.Lockfile, rw) 138 if err != nil { 139 return inPlaceResolutionMsg{err: err} 140 } 141 142 resolved, err := remediation.ResolveGraphVulns(context.Background(), opts.ResolveClient, opts.VulnEnricher, g, nil, &opts.RemediationOptions) 143 if err != nil { 144 return inPlaceResolutionMsg{err: fmt.Errorf("failed resolving lockfile vulnerabilities: %w", err)} 145 } 146 allPatches, err := inplace.ComputePatches(context.Background(), opts.ResolveClient, resolved, &opts.RemediationOptions) 147 if err != nil { 148 return inPlaceResolutionMsg{err: fmt.Errorf("failed computing patches: %w", err)} 149 } 150 return inPlaceResolutionMsg{resolvedGraph: resolved, allPatches: allPatches} 151 } 152 } 153 154 type doRelockMsg struct { 155 resolvedManifest *remediation.ResolvedManifest 156 err error 157 } 158 159 func doInitialRelockCmd(opts options.FixVulnsOptions, rw manifest.ReadWriter) tea.Cmd { 160 return func() tea.Msg { 161 m, err := parser.ParseManifest(opts.Manifest, rw) 162 if err != nil { 163 return doRelockMsg{err: err} 164 } 165 if opts.DepCachePopulator != nil { 166 opts.DepCachePopulator.PopulateCache(context.Background(), opts.ResolveClient, m.Requirements(), opts.Manifest) 167 } 168 resolved, err := remediation.ResolveManifest(context.Background(), opts.ResolveClient, opts.VulnEnricher, m, &opts.RemediationOptions) 169 if err != nil { 170 return doRelockMsg{err: fmt.Errorf("failed resolving manifest vulnerabilities: %w", err)} 171 } 172 return doRelockMsg{resolvedManifest: resolved} 173 } 174 } 175 176 func computeResolveErrors(g *resolve.Graph) []result.ResolveError { 177 var errs []result.ResolveError 178 for _, n := range g.Nodes { 179 for _, e := range n.Errors { 180 errs = append(errs, result.ResolveError{ 181 Package: result.Package{ 182 Name: n.Version.Name, 183 Version: n.Version.Version, 184 }, 185 Requirement: result.Package{ 186 Name: e.Req.Name, 187 Version: e.Req.Version, 188 }, 189 Error: e.Error, 190 }) 191 } 192 } 193 194 return errs 195 }