|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "sort" |
| 7 | + "text/tabwriter" |
| 8 | + |
| 9 | + "github.com/spf13/cobra" |
| 10 | + |
| 11 | + "github.com/tronprotocol/tron-deployment/internal/build" |
| 12 | + "github.com/tronprotocol/tron-deployment/internal/output" |
| 13 | +) |
| 14 | + |
| 15 | +// `trond build list` enumerates the on-disk build cache so operators |
| 16 | +// (and agents via `--output json`) can see what artifacts are taking |
| 17 | +// space, when they were built, and from which source revision — |
| 18 | +// without poking under the state directory by hand. |
| 19 | +// |
| 20 | +// Default behavior hides orphans (manifests whose artifact has been |
| 21 | +// deleted out-of-band). `--include-orphans` surfaces them too, which |
| 22 | +// is the data shape `trond build prune --orphan` operates on. |
| 23 | +// |
| 24 | +// Output schema: schemas/output/build-list.schema.json. |
| 25 | + |
| 26 | +var ( |
| 27 | + buildListFilter string |
| 28 | + buildListSort string |
| 29 | + buildListIncludeOrphans bool |
| 30 | +) |
| 31 | + |
| 32 | +var buildListCmd = &cobra.Command{ |
| 33 | + Use: "list", |
| 34 | + Short: "List cached build artifacts", |
| 35 | + Long: `Walk the trond build cache directory and emit one row per |
| 36 | +cached artifact (JAR or image). Useful for finding the cache key to |
| 37 | +reference in trond build inspect / prune, or for spotting orphaned |
| 38 | +manifests whose underlying artifact has been deleted out-of-band.`, |
| 39 | + Example: ` # Table view, newest first. |
| 40 | + trond build list |
| 41 | +
|
| 42 | + # Just images, JSON for piping into jq. |
| 43 | + trond build list --filter image -o json |
| 44 | +
|
| 45 | + # Sort by size to find the biggest cache hogs. |
| 46 | + trond build list --sort size`, |
| 47 | + RunE: runBuildList, |
| 48 | +} |
| 49 | + |
| 50 | +func init() { |
| 51 | + buildListCmd.Flags().StringVar(&buildListFilter, "filter", "all", |
| 52 | + "Artifact kind to include: 'all' (default), 'jar', or 'image'") |
| 53 | + buildListCmd.Flags().StringVar(&buildListSort, "sort", "newest", |
| 54 | + "Sort order: 'newest' (default), 'oldest', or 'size' (largest first)") |
| 55 | + buildListCmd.Flags().BoolVar(&buildListIncludeOrphans, "include-orphans", false, |
| 56 | + "Include cache entries whose underlying artifact is missing") |
| 57 | + buildCmd.AddCommand(buildListCmd) |
| 58 | +} |
| 59 | + |
| 60 | +func runBuildList(cmd *cobra.Command, _ []string) error { |
| 61 | + opts := []build.ListOption{} |
| 62 | + if buildListIncludeOrphans { |
| 63 | + opts = append(opts, build.IncludeOrphans()) |
| 64 | + } |
| 65 | + entries, err := build.ListEntries(cmd.Context(), opts...) |
| 66 | + if err != nil { |
| 67 | + return output.NewErrorf("LIST_ERROR", output.ExitGeneralError, |
| 68 | + "list build cache: %s", err.Error()) |
| 69 | + } |
| 70 | + |
| 71 | + // Apply --filter and --sort here at the CLI layer so the |
| 72 | + // library's ListEntries stays a thin walker. (--filter is |
| 73 | + // post-walk: walking is cheap, image-size lookups already |
| 74 | + // happened; filtering after is straightforward.) |
| 75 | + entries = filterEntriesByKind(entries, buildListFilter) |
| 76 | + if err := sortEntries(entries, buildListSort); err != nil { |
| 77 | + return output.NewError("VALIDATION_ERROR", output.ExitValidationError, err.Error()) |
| 78 | + } |
| 79 | + |
| 80 | + outputFmt, _ := cmd.Flags().GetString("output") |
| 81 | + if outputFmt == "json" { |
| 82 | + return output.WriteJSON(os.Stdout, map[string]any{ |
| 83 | + "entries": entries, |
| 84 | + "count": len(entries), |
| 85 | + }) |
| 86 | + } |
| 87 | + return printBuildListTable(entries) |
| 88 | +} |
| 89 | + |
| 90 | +func filterEntriesByKind(entries []*build.Entry, filter string) []*build.Entry { |
| 91 | + if filter == "" || filter == "all" { |
| 92 | + return entries |
| 93 | + } |
| 94 | + out := make([]*build.Entry, 0, len(entries)) |
| 95 | + for _, e := range entries { |
| 96 | + if e.ArtifactKind == filter { |
| 97 | + out = append(out, e) |
| 98 | + } |
| 99 | + } |
| 100 | + return out |
| 101 | +} |
| 102 | + |
| 103 | +func sortEntries(entries []*build.Entry, order string) error { |
| 104 | + switch order { |
| 105 | + case "", "newest": |
| 106 | + // ListEntries already returned newest-first; no-op. |
| 107 | + return nil |
| 108 | + case "oldest": |
| 109 | + sort.SliceStable(entries, func(i, j int) bool { |
| 110 | + return entries[i].CreatedAt.Before(entries[j].CreatedAt) |
| 111 | + }) |
| 112 | + return nil |
| 113 | + case "size": |
| 114 | + sort.SliceStable(entries, func(i, j int) bool { |
| 115 | + return entries[i].SizeBytes > entries[j].SizeBytes |
| 116 | + }) |
| 117 | + return nil |
| 118 | + default: |
| 119 | + return fmt.Errorf("invalid --sort %q (want: newest|oldest|size)", order) |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +func printBuildListTable(entries []*build.Entry) error { |
| 124 | + if len(entries) == 0 { |
| 125 | + fmt.Println("No cached builds.") |
| 126 | + return nil |
| 127 | + } |
| 128 | + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
| 129 | + fmt.Fprintln(tw, "CACHE_KEY\tKIND\tSOURCE_REV\tSIZE\tCREATED\tARTIFACT") |
| 130 | + for _, e := range entries { |
| 131 | + artifact := e.ImageTag |
| 132 | + if e.ArtifactKind == "jar" { |
| 133 | + artifact = e.ArtifactPath |
| 134 | + } |
| 135 | + marker := "" |
| 136 | + if e.Orphaned { |
| 137 | + marker = " (orphan)" |
| 138 | + } |
| 139 | + fmt.Fprintf(tw, "%s\t%s%s\t%s\t%s\t%s\t%s\n", |
| 140 | + e.CacheKey, |
| 141 | + e.ArtifactKind, marker, |
| 142 | + shortRev(e.SourceRevision), |
| 143 | + humanBytes(e.SizeBytes), |
| 144 | + e.CreatedAt.Local().Format("2006-01-02 15:04"), |
| 145 | + artifact, |
| 146 | + ) |
| 147 | + } |
| 148 | + return tw.Flush() |
| 149 | +} |
| 150 | + |
| 151 | +// shortRev trims a 40-char git sha to 12 chars for table readability. |
| 152 | +func shortRev(rev string) string { |
| 153 | + if len(rev) > 12 { |
| 154 | + return rev[:12] |
| 155 | + } |
| 156 | + return rev |
| 157 | +} |
| 158 | + |
| 159 | +// humanBytes formats a byte count as a short string (e.g. "615MB"). |
| 160 | +// Operators don't need bytes precision in a table — JSON output |
| 161 | +// still carries the raw size_bytes for tooling. |
| 162 | +func humanBytes(n int64) string { |
| 163 | + const ( |
| 164 | + kib = 1024 |
| 165 | + mib = kib * 1024 |
| 166 | + gib = mib * 1024 |
| 167 | + ) |
| 168 | + switch { |
| 169 | + case n >= gib: |
| 170 | + return fmt.Sprintf("%.1fGB", float64(n)/float64(gib)) |
| 171 | + case n >= mib: |
| 172 | + return fmt.Sprintf("%dMB", n/mib) |
| 173 | + case n >= kib: |
| 174 | + return fmt.Sprintf("%dKB", n/kib) |
| 175 | + case n == 0: |
| 176 | + return "-" |
| 177 | + default: |
| 178 | + return fmt.Sprintf("%dB", n) |
| 179 | + } |
| 180 | +} |
0 commit comments