package utils import ( "archive/tar" "compress/gzip" "context" "fmt" "io" "os" "path/filepath" "strconv" "strings" "time" "github.com/rs/zerolog/log" ) const confirmChar = "Y" func addToArchive(tw *tar.Writer, filename string) error { file, err := os.Open(filename) if err != nil { return err } defer file.Close() //nolint: errcheck // defered info, err := file.Stat() if err != nil { return err } header, err := tar.FileInfoHeader(info, info.Name()) if err != nil { return err } header.Name = filepath.Base(file.Name()) if err = tw.WriteHeader(header); err != nil { return err } _, err = io.Copy(tw, file) return err } // CreateArchive creates a gzip tar archive in the `destDir` path including `files`. func CreateArchive(destDir, name string, files ...string) (string, error) { now := time.Now().UTC() archivePath := filepath.Join( destDir, fmt.Sprintf("%s-%s.tar.gz", name, strings.ReplaceAll(now.Format(time.RFC3339), ":", "-")), ) file, err := os.Create(archivePath) if err != nil { return "", fmt.Errorf("unable to create archive=%s, err=%v", archivePath, err) } defer file.Close() //nolint: errcheck // defered gw := gzip.NewWriter(file) defer gw.Close() //nolint: errcheck // defered tw := tar.NewWriter(gw) defer tw.Close() //nolint: errcheck // defered for _, f := range files { if err := addToArchive(tw, f); err != nil { return "", fmt.Errorf( "unable to add file=%s to archive=%s, err=%v", f, archivePath, err, ) } } return archivePath, nil } func Confirm(ctx context.Context, destroy bool) error { logMsg := "deploy" if destroy { logMsg = "undeploy" } log.Warn().Msg(fmt.Sprintf("Confirm to %s ? Y to confirm", logMsg)) var text string if _, err := fmt.Fscanf(os.Stdin, "%s", &text); err != nil { if !strings.Contains(err.Error(), "newline") { return err } } if !strings.EqualFold(text, confirmChar) { log.Info().Msg("Ok, bye !") os.Exit(0) } return nil } // CatchAndConvertNumber finds and converts the first consecutives digits in integer. func CatchAndConvertNumber(value string) (int, error) { buf := []rune{} hasLeftTrimmed := false for _, c := range value { if c >= '0' && c <= '9' { hasLeftTrimmed = true buf = append(buf, c) continue } // early return if no number catch if hasLeftTrimmed { break } } return strconv.Atoi(string(buf)) } type HeaderColumn struct { Name string Length int pos int } type Header struct { BorderStyle string headerMeta map[string]*HeaderColumn Columns []HeaderColumn } type TableOption func(*Table) func WithHeaderBorderStyle(style string) TableOption { return func(t *Table) { t.Header.BorderStyle = style } } func WithRowSeparator(separator string) TableOption { return func(t *Table) { t.RowSeparator = separator } } func WithColSeparator(separator string) TableOption { return func(t *Table) { t.ColSeparator = separator } } func WithHeader(name string, length int) TableOption { return func(t *Table) { if t.Header.Columns == nil { t.Header.Columns = []HeaderColumn{} } pos := len(t.Header.Columns) t.Header.Columns = append( t.Header.Columns, HeaderColumn{Name: name, Length: length, pos: pos}, ) if t.Header.headerMeta == nil { t.Header.headerMeta = map[string]*HeaderColumn{} } t.Header.headerMeta[strings.ToLower(name)] = &t.Header.Columns[pos] } } type Column struct { Name string Value string } type Table struct { RowSeparator string ColSeparator string data [][]string Header Header cursor int } func NewTable(options ...TableOption) Table { table := Table{} for _, o := range options { o(&table) } table.data = [][]string{} return table } func (t *Table) AddRow(columns ...Column) error { maxNbCols := len(t.Header.Columns) if len(columns) > maxNbCols { return fmt.Errorf("invalid column number, should be %d", maxNbCols) } rowData := make([]string, maxNbCols) t.data = append(t.data, rowData) for idx := range columns { header, ok := t.Header.headerMeta[strings.ToLower(columns[idx].Name)] if !ok { return fmt.Errorf("no corresponding %s column exist", columns[idx].Name) } value := columns[idx].Value if len(value) > header.Length { log.Debug(). Str("column", columns[idx].Name). Str("value", value). Msg("col value too long, trimming...") value = value[:header.Length-3] + "..." } t.data[t.cursor][header.pos] = value } t.cursor++ return nil } func (t *Table) Render() { table := []string{} // header headerParts := []string{} for idx := range t.Header.Columns { headerParts = append( headerParts, fmt.Sprintf("%-*s", t.Header.Columns[idx].Length, t.Header.Columns[idx].Name), ) } header := strings.Join(headerParts, t.ColSeparator) border := "" for i := 0; i < len(header); i++ { border += t.Header.BorderStyle } rowHr := "" for i := 0; i < len(header); i++ { rowHr += t.RowSeparator } table = append(table, border, header, border) // data for idx := range t.data { lineParts := []string{} for idy := range t.data[idx] { lineParts = append(lineParts, fmt.Sprintf( "%-*s", t.Header.Columns[idy].Length, t.data[idx][idy]), ) } if idx+1 < len(t.data) { table = append(table, strings.Join(lineParts, t.ColSeparator), rowHr) } } fmt.Println(strings.Join(table, "\n")) }