github.com/saucelabs/saucectl@v0.175.1/internal/cmd/imagerunner/artifacts.go (about) 1 package imagerunner 2 3 import ( 4 "archive/zip" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "os" 10 11 "github.com/jedib0t/go-pretty/v6/table" 12 "github.com/jedib0t/go-pretty/v6/text" 13 "github.com/ryanuber/go-glob" 14 szip "github.com/saucelabs/saucectl/internal/archive/zip" 15 cmds "github.com/saucelabs/saucectl/internal/cmd" 16 "github.com/saucelabs/saucectl/internal/fileio" 17 "github.com/saucelabs/saucectl/internal/http" 18 "github.com/saucelabs/saucectl/internal/imagerunner" 19 "github.com/saucelabs/saucectl/internal/segment" 20 "github.com/saucelabs/saucectl/internal/usage" 21 "github.com/spf13/cobra" 22 "golang.org/x/text/cases" 23 "golang.org/x/text/language" 24 ) 25 26 var defaultTableStyle = table.Style{ 27 Name: "saucy", 28 Box: table.BoxStyle{ 29 BottomLeft: "└", 30 BottomRight: "┘", 31 BottomSeparator: "", 32 EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), 33 Left: "│", 34 LeftSeparator: "", 35 MiddleHorizontal: "─", 36 MiddleSeparator: "", 37 MiddleVertical: "", 38 PaddingLeft: " ", 39 PaddingRight: " ", 40 PageSeparator: "\n", 41 Right: "│", 42 RightSeparator: "", 43 TopLeft: "┌", 44 TopRight: "┐", 45 TopSeparator: "", 46 UnfinishedRow: " ...", 47 }, 48 Color: table.ColorOptionsDefault, 49 Format: table.FormatOptions{ 50 Footer: text.FormatDefault, 51 Header: text.FormatDefault, 52 Row: text.FormatDefault, 53 }, 54 HTML: table.DefaultHTMLOptions, 55 Options: table.Options{ 56 DrawBorder: true, 57 SeparateColumns: false, 58 SeparateFooter: true, 59 SeparateHeader: true, 60 SeparateRows: false, 61 }, 62 Title: table.TitleOptionsDefault, 63 } 64 65 func ArtifactsCommand() *cobra.Command { 66 cmd := &cobra.Command{ 67 Use: "artifacts", 68 Short: "Commands for interacting with artifacts produced by the imagerunner.", 69 SilenceUsage: true, 70 } 71 72 cmd.AddCommand( 73 downloadCommand(), 74 ) 75 76 return cmd 77 } 78 79 func downloadCommand() *cobra.Command { 80 var targetDir string 81 var out string 82 83 cmd := &cobra.Command{ 84 Use: "download <runID> <file-pattern>", 85 Short: "Downloads the specified artifacts from the given run. Supports glob pattern.", 86 SilenceUsage: true, 87 Args: func(cmd *cobra.Command, args []string) error { 88 if len(args) == 0 || args[0] == "" { 89 return errors.New("no run ID specified") 90 } 91 if len(args) == 1 || args[1] == "" { 92 return errors.New("no file pattern specified") 93 } 94 95 return nil 96 }, 97 PreRunE: func(cmd *cobra.Command, args []string) error { 98 err := http.CheckProxy() 99 if err != nil { 100 return fmt.Errorf("invalid HTTP_PROXY value") 101 } 102 103 tracker := segment.DefaultTracker 104 105 go func() { 106 tracker.Collect( 107 cases.Title(language.English).String(cmds.FullName(cmd)), 108 usage.Properties{}.SetFlags(cmd.Flags()), 109 ) 110 _ = tracker.Close() 111 }() 112 return nil 113 }, 114 RunE: func(cmd *cobra.Command, args []string) error { 115 ID := args[0] 116 filePattern := args[1] 117 118 return download(ID, filePattern, targetDir, out) 119 }, 120 } 121 122 flags := cmd.Flags() 123 flags.StringVar(&targetDir, "target-dir", "", "Save files to target directory. Defaults to current working directory.") 124 flags.StringVarP(&out, "out", "o", "text", "Output format to the console. Options: text, json.") 125 126 return cmd 127 } 128 129 func download(ID, filePattern, targetDir, outputFormat string) error { 130 reader, err := imagerunnerClient.DownloadArtifacts(context.Background(), ID) 131 if err != nil { 132 return fmt.Errorf("failed to fetch artifacts: %w", err) 133 } 134 defer reader.Close() 135 136 fileName, err := fileio.CreateTemp(reader) 137 if err != nil { 138 return fmt.Errorf("failed to download artifacts content: %w", err) 139 } 140 defer os.Remove(fileName) 141 142 zf, err := zip.OpenReader(fileName) 143 if err != nil { 144 return fmt.Errorf("failed to open file: %w", err) 145 } 146 defer zf.Close() 147 148 files := []string{} 149 for _, f := range zf.File { 150 if glob.Glob(filePattern, f.Name) { 151 files = append(files, f.Name) 152 if err = szip.Extract(targetDir, f); err != nil { 153 return fmt.Errorf("failed to extract file: %w", err) 154 } 155 } 156 } 157 158 lst := imagerunner.ArtifactList{ 159 ID: ID, 160 Items: files, 161 } 162 163 switch outputFormat { 164 case "json": 165 if err := renderJSON(lst); err != nil { 166 return fmt.Errorf("failed to render output: %w", err) 167 } 168 case "text": 169 renderTable(lst) 170 default: 171 return errors.New("unknown output format") 172 } 173 return nil 174 } 175 176 func renderTable(lst imagerunner.ArtifactList) { 177 if len(lst.Items) == 0 { 178 println("No artifacts for this job.") 179 return 180 } 181 182 t := table.NewWriter() 183 t.SetStyle(defaultTableStyle) 184 t.SuppressEmptyColumns() 185 186 t.AppendHeader(table.Row{"Items"}) 187 t.SetColumnConfigs([]table.ColumnConfig{ 188 { 189 Name: "Items", 190 }, 191 }) 192 193 for _, item := range lst.Items { 194 // the order of values must match the order of the header 195 t.AppendRow(table.Row{item}) 196 } 197 198 fmt.Println(t.Render()) 199 } 200 201 func renderJSON(val any) error { 202 return json.NewEncoder(os.Stdout).Encode(val) 203 }