From 72d2d692cccb2a4b18ea7bd224fb18c82eb6ddc6 Mon Sep 17 00:00:00 2001 From: Michael McCracken Date: Wed, 13 Dec 2023 14:56:20 -0800 Subject: [PATCH 1/2] WIP: add a layers subtree for image layers some TODOs, still need to think harder about the displaystring, and add a useful summary. also want to be able to toggle these globally, it's a lot to show them for every image Signed-off-by: Michael McCracken --- ociutils.go | 35 +++++++++++++++++++++++++++++++ ui.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/ociutils.go b/ociutils.go index d8e866f..6d31ae3 100644 --- a/ociutils.go +++ b/ociutils.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "os" + "os/exec" "path/filepath" "strings" "text/tabwriter" @@ -121,6 +122,40 @@ func (sr subIndexRef) summary() string { return fmt.Sprintf("sub-index with %d manifests", len(info.manifestDescriptors)) } +type layerRef struct { + blobfilepath string + hash string + displayString string +} + +var LayerSummaryCache = map[string]string{} + +func (lr layerRef) summary(filter string) string { + layerfilterkey := lr.blobfilepath + "\\" + filter + cachedSummary, ok := LayerSummaryCache[layerfilterkey] + if ok { + return cachedSummary + } + + filterArg := "" + if filter != "" { + filterArg = " | grep " + filter + } + cmdstr := "tar tzvf " + lr.blobfilepath + filterArg + cmd := exec.Command("sh", "-c", cmdstr) + var out strings.Builder + var stderr strings.Builder + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + log.Printf("error: %v", err) + } + summaryString := fmt.Sprintf("%q\n%q\n%s\n%s", lr.displayString, cmdstr, out.String(), stderr.String()) + LayerSummaryCache[layerfilterkey] = summaryString + return summaryString +} + // ImageInfoMap - global map of manifest hashes to info var ImageInfoMap = map[string]imageInfo{} diff --git a/ui.go b/ui.go index 967342b..94f5dfa 100644 --- a/ui.go +++ b/ui.go @@ -110,6 +110,25 @@ func addContentsOfLayout(target *tview.TreeNode, path string) ([]imageInfo, []su node.AddChild(refNode) } + + layerTreeNode := tview.NewTreeNode("layers"). + SetReference(imageInfo.ref). + SetSelectable(true) + node.AddChild(layerTreeNode) + + for _, layerDigest := range imageInfo.layerDigests { + displayString := layerDigest + if len(LayerNameMap[layerDigest]) > 0 { + displayString = strings.Join(LayerNameMap[layerDigest], ",") + } + blobfilepath := filepath.Join(path, "blobs", "sha256", layerDigest) + newLayerRef := layerRef{hash: layerDigest, blobfilepath: blobfilepath, displayString: displayString} + layerNode := tview.NewTreeNode(displayString). + SetReference(newLayerRef). + SetSelectable(true) + layerTreeNode.AddChild(layerNode) + } + target.AddChild(node) log.Printf(" done loading image %q", imageInfo.displayName) } @@ -382,6 +401,9 @@ func getMatchingTreeNodes(node *tview.TreeNode, needle string) []*tview.TreeNode case imageref: haystacks = reference.(imageref).searchString() + case layerRef: + haystacks = []string{ref.hash, ref.displayString} + case subIndexRef: haystacks = []string{} info := SubIndexInfoMap[ref.hash] @@ -435,6 +457,8 @@ func clearTreeFormatting(node *tview.TreeNode, selectable bool) { node.SetColor(tcell.ColorBlue) case imageref: node.SetColor(tcell.ColorRed) + case layerRef: + node.SetColor(tcell.ColorGreen) case subIndexRef: node.SetColor(tcell.ColorBlue) default: @@ -550,7 +574,32 @@ func doTViewStuff(ctxt *cli.Context) error { SetText(strings.Join(summaries, "\n")). SetDynamicColors(true). SetRegions(true) + infoPane.Box.SetBorder(true) + currentFilter := "" + summaryFilterField := tview.NewInputField(). + SetLabel("Filter Output: "). + SetChangedFunc(func(needle string) { + + currentFilter = needle + + reference := tree.GetCurrentNode().GetReference() + switch ref := reference.(type) { + case layerRef: + infoPane.SetText(ref.summary(currentFilter)) + } + + // update info pane with summaries + }) + summaryFilterField.SetDoneFunc(func(key tcell.Key) { + app.SetFocus(infoPane) + }) + + summaryFilterField.Box.SetBorder(true) + + infoPaneGrid := tview.NewGrid().SetRows(0, 3).SetColumns(0). + AddItem(infoPane, 0, 0, 1, 1, 0, 0, true). + AddItem(summaryFilterField, 1, 0, 1, 1, 0, 0, false) statusLine := tview.NewTextView(). SetTextAlign(tview.AlignCenter). @@ -571,6 +620,8 @@ func doTViewStuff(ctxt *cli.Context) error { infoPane.ScrollToBeginning() case treeInfo: infoPane.SetText(tview.Escape(ref.summary())) + case layerRef: + infoPane.SetText(ref.summary(currentFilter)) case subIndexRef: infoPane.SetText(ref.summary()) default: @@ -584,6 +635,10 @@ func doTViewStuff(ctxt *cli.Context) error { case treeInfo: infoPane.SetText(ref.summary()) infoPane.ScrollToBeginning() + case layerRef: + // todo mmcc didn't think through this behavior: + infoPane.SetText(ref.summary(currentFilter)) + infoPane.ScrollToBeginning() case subIndexRef: infoPane.SetText(ref.summary()) default: @@ -634,10 +689,10 @@ func doTViewStuff(ctxt *cli.Context) error { SetRows(0, 1). SetColumns(-1, -3). AddItem(treeGrid, 0, 0, 1, 1, 0, 0, true). - AddItem(infoPane, 0, 1, 1, 1, 0, 0, false). + AddItem(infoPaneGrid, 0, 1, 1, 1, 0, 0, false). AddItem(statusLine, 1, 0, 1, 2, 0, 0, false) - tabbableViews := []tview.Primitive{tree, searchInputField, infoPane} + tabbableViews := []tview.Primitive{tree, searchInputField, infoPane, summaryFilterField} tabbableViewIdx := 0 setNewFocusedViewIdx := func(prevIdx int, newIdx int) { From 3f799e6562a161eb147ab0464f2d374eb3d482ee Mon Sep 17 00:00:00 2001 From: Michael McCracken Date: Wed, 12 Jun 2024 12:17:12 -0700 Subject: [PATCH 2/2] add start of support for unsquash in layer contents doesn't work yet. Signed-off-by: Michael McCracken --- ociutils.go | 6 ++++++ ui.go | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ociutils.go b/ociutils.go index 6d31ae3..a13e71f 100644 --- a/ociutils.go +++ b/ociutils.go @@ -126,6 +126,7 @@ type layerRef struct { blobfilepath string hash string displayString string + mediaType string } var LayerSummaryCache = map[string]string{} @@ -141,7 +142,12 @@ func (lr layerRef) summary(filter string) string { if filter != "" { filterArg = " | grep " + filter } + cmdstr := "tar tzvf " + lr.blobfilepath + filterArg + if strings.Contains(lr.mediaType, "squashfs") { + cmdstr = "echo TODO: gunzip -c " + lr.blobfilepath + " |unsquashfs -llc " + //TODO: can't unsquash via pipe, need to gunzip to tempfile and delete + } cmd := exec.Command("sh", "-c", cmdstr) var out strings.Builder var stderr strings.Builder diff --git a/ui.go b/ui.go index 94f5dfa..ac7b1b2 100644 --- a/ui.go +++ b/ui.go @@ -116,13 +116,15 @@ func addContentsOfLayout(target *tview.TreeNode, path string) ([]imageInfo, []su SetSelectable(true) node.AddChild(layerTreeNode) - for _, layerDigest := range imageInfo.layerDigests { + for idx, layerDigest := range imageInfo.layerDigests { displayString := layerDigest if len(LayerNameMap[layerDigest]) > 0 { displayString = strings.Join(LayerNameMap[layerDigest], ",") } blobfilepath := filepath.Join(path, "blobs", "sha256", layerDigest) - newLayerRef := layerRef{hash: layerDigest, blobfilepath: blobfilepath, displayString: displayString} + mt := imageInfo.manifest.Layers[idx].MediaType + newLayerRef := layerRef{hash: layerDigest, mediaType: mt, + blobfilepath: blobfilepath, displayString: displayString} layerNode := tview.NewTreeNode(displayString). SetReference(newLayerRef). SetSelectable(true)