github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/dot.go (about) 1 package cli 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "net/http" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/spf13/cobra" 16 "github.com/tmc/dot" 17 "github.com/wolfi-dev/wolfictl/pkg/dag" 18 "golang.org/x/sync/errgroup" 19 20 "github.com/skratchdot/open-golang/open" 21 ) 22 23 func cmdSVG() *cobra.Command { //nolint:gocyclo 24 var dir, pipelineDir string 25 var showDependents, recursive, span, web bool 26 var extraKeys, extraRepos []string 27 d := &cobra.Command{ 28 Use: "dot", 29 Short: "Generate graphviz .dot output", 30 Args: cobra.MinimumNArgs(1), 31 Long: ` 32 Generate .dot output and pipe it to dot to generate an SVG 33 34 wolfictl dot zlib | dot -Tsvg > graph.svg 35 36 Generate .dot output and pipe it to dot to generate a PNG 37 38 wolfictl dot zlib | dot -Tpng > graph.png 39 40 Open browser to explore crane 41 42 wolfictl dot --web crane 43 44 Open browser to explore crane's deps recursively, only showing a minimum subgraph 45 46 wolfictl dot --web -R -S crane 47 `, 48 RunE: func(cmd *cobra.Command, args []string) error { 49 ctx := cmd.Context() 50 if pipelineDir == "" { 51 pipelineDir = filepath.Join(dir, "pipelines") 52 } 53 54 pkgs, err := dag.NewPackages(ctx, os.DirFS(dir), dir, pipelineDir) 55 if err != nil { 56 return fmt.Errorf("NewPackages: %w", err) 57 } 58 59 g, err := dag.NewGraph(ctx, pkgs, 60 dag.WithKeys(extraKeys...), 61 dag.WithRepos(extraRepos...), 62 ) 63 if err != nil { 64 return fmt.Errorf("building graph: %w", err) 65 } 66 67 amap, err := g.Graph.AdjacencyMap() 68 if err != nil { 69 return err 70 } 71 72 pmap, err := g.Graph.PredecessorMap() 73 if err != nil { 74 return err 75 } 76 77 render := func(args []string) (*dot.Graph, error) { 78 todo := []string{} 79 queued := map[string]struct{}{} 80 81 out := dot.NewGraph("images") 82 if err := out.Set("rankdir", "LR"); err != nil { 83 return nil, err 84 } 85 out.SetType(dot.DIGRAPH) 86 87 renderNode := func(node string) error { 88 var byName []dag.Package 89 config := pkgs.ConfigByKey(node) 90 if config != nil { 91 byName = append(byName, config) 92 } else { 93 byName, err = g.NodesByName(node) 94 if err != nil { 95 return err 96 } 97 98 if len(byName) == 0 { 99 return fmt.Errorf("could not find node %q", node) 100 } 101 } 102 103 for _, name := range byName { 104 h := dag.PackageHash(name) 105 106 pkgver, source := split(h) 107 n := dot.NewNode(pkgver) 108 if err := n.Set("tooltip", source); err != nil { 109 return err 110 } 111 if pkgs.ConfigByKey(pkgver) != nil { 112 if web { 113 if err := n.Set("URL", link(args, pkgver)); err != nil { 114 return err 115 } 116 } 117 } else { 118 if err := n.Set("color", "red"); err != nil { 119 return err 120 } 121 } 122 out.AddNode(n) 123 124 dependencies, ok := amap[h] 125 if !ok { 126 continue 127 } 128 129 deps := make([]string, 0, len(dependencies)) 130 for dep := range dependencies { 131 deps = append(deps, dep) 132 } 133 sort.Strings(deps) 134 135 for _, dep := range deps { 136 if recursive || span { 137 if _, ok := queued[dep]; ok { 138 if span { 139 continue 140 } 141 } else { 142 todo = append(todo, dep) 143 queued[dep] = struct{}{} 144 } 145 } 146 147 pkgver, source := split(dep) 148 d := dot.NewNode(pkgver) 149 if err := d.Set("tooltip", source); err != nil { 150 return err 151 } 152 if pkgs.ConfigByKey(pkgver) != nil { 153 if web { 154 if err := d.Set("URL", link(args, pkgver)); err != nil { 155 return err 156 } 157 } 158 } else { 159 if err := d.Set("color", "red"); err != nil { 160 return err 161 } 162 } 163 out.AddNode(d) 164 out.AddEdge(dot.NewEdge(n, d)) 165 } 166 167 if !showDependents { 168 continue 169 } 170 171 predecessors, ok := pmap[h] 172 if !ok { 173 continue 174 } 175 176 preds := make([]string, 0, len(predecessors)) 177 for pred := range predecessors { 178 preds = append(preds, pred) 179 } 180 sort.Strings(preds) 181 182 for _, pred := range preds { 183 pkgver, source := split(pred) 184 d := dot.NewNode(pkgver) 185 if err := d.Set("tooltip", source); err != nil { 186 return err 187 } 188 if pkgs.ConfigByKey(pkgver) != nil { 189 if web { 190 if err := d.Set("URL", link(args, pkgver)); err != nil { 191 return err 192 } 193 } 194 } 195 out.AddNode(d) 196 out.AddEdge(dot.NewEdge(d, n)) 197 } 198 } 199 200 return nil 201 } 202 203 for _, node := range args { 204 if err := renderNode(node); err != nil { 205 return nil, err 206 } 207 } 208 209 if recursive { 210 var node string 211 for len(todo) != 0 { 212 node, todo = pop(todo) 213 214 pkgver, _ := split(node) 215 if pkgs.ConfigByKey(pkgver) == nil { 216 continue 217 } 218 219 if err := renderNode(pkgver); err != nil { 220 return nil, err 221 } 222 } 223 } 224 225 return out, nil 226 } 227 228 if web { 229 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 230 if r.URL.Path != "/" { 231 return 232 } 233 nodes := r.URL.Query()["node"] 234 235 if len(nodes) == 0 { 236 nodes = args 237 } 238 239 out, err := render(nodes) 240 if err != nil { 241 fmt.Fprintf(w, "error rendering %v: %v", nodes, err) 242 log.Fatal(err) 243 } 244 245 log.Printf("%s: rendering %v", r.URL, nodes) 246 cmd := exec.Command("dot", "-Tsvg") 247 cmd.Stdin = strings.NewReader(out.String()) 248 cmd.Stdout = w 249 250 if err := cmd.Run(); err != nil { 251 fmt.Fprintf(w, "error rendering %v: %v", nodes, err) 252 log.Fatal(err) 253 } 254 }) 255 256 l, err := net.Listen("tcp", "127.0.0.1:0") 257 if err != nil { 258 return err 259 } 260 261 server := &http.Server{ 262 Addr: l.Addr().String(), 263 ReadHeaderTimeout: 3 * time.Second, 264 } 265 266 log.Printf("%s", l.Addr().String()) 267 268 var g errgroup.Group 269 g.Go(func() error { 270 return server.Serve(l) 271 }) 272 273 g.Go(func() error { 274 return open.Run(fmt.Sprintf("http://localhost:%d", l.Addr().(*net.TCPAddr).Port)) 275 }) 276 277 g.Go(func() error { 278 <-ctx.Done() 279 server.Close() 280 return ctx.Err() 281 }) 282 283 return g.Wait() 284 } 285 286 out, err := render(args) 287 if err != nil { 288 return err 289 } 290 291 fmt.Println(out.String()) 292 return nil 293 }, 294 } 295 d.Flags().StringVarP(&dir, "dir", "d", ".", "directory to search for melange configs") 296 d.Flags().StringVar(&pipelineDir, "pipeline-dir", "", "directory used to extend defined built-in pipelines") 297 d.Flags().BoolVarP(&showDependents, "show-dependents", "D", false, "show packages that depend on these packages, instead of these packages' dependencies") 298 d.Flags().BoolVarP(&recursive, "recursive", "R", false, "recurse through package dependencies") 299 d.Flags().BoolVarP(&span, "spanning-tree", "S", false, "does something like a spanning tree to avoid a huge number of edges") 300 d.Flags().StringSliceVarP(&extraKeys, "keyring-append", "k", []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"}, "path to extra keys to include in the build environment keyring") 301 d.Flags().StringSliceVarP(&extraRepos, "repository-append", "r", []string{"https://packages.wolfi.dev/os"}, "path to extra repositories to include in the build environment") 302 d.Flags().BoolVar(&web, "web", false, "do a website") 303 return d 304 } 305 306 func split(in string) (pkgver, source string) { 307 before, source, ok := strings.Cut(in, "@") 308 if !ok { 309 panic(in) 310 } 311 312 pkg, ver, ok := strings.Cut(before, ":") 313 if !ok { 314 panic(in) 315 } 316 317 return fmt.Sprintf("%s-%s", pkg, ver), source 318 } 319 320 func pop(a []string) (result string, stack []string) { 321 return a[len(a)-1], a[:len(a)-1] 322 } 323 324 func link(args []string, pkgver string) string { 325 filtered := []string{} 326 for _, a := range args { 327 if a != pkgver { 328 filtered = append(filtered, a) 329 } 330 } 331 return "/?node=" + pkgver + "&node=" + strings.Join(filtered, "&node=") 332 }