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  }