From bfaf6f10c619afd222c6eef5bdb6f2954248fd07 Mon Sep 17 00:00:00 2001 From: rmanach Date: Tue, 29 Apr 2025 15:59:57 +0200 Subject: [PATCH] format table + add extra fields --- docker/models.go | 220 ++++++++++++++++++----------------------------- main.go | 74 +++++++++------- utils/utils.go | 125 ++++++++++++++++----------- 3 files changed, 204 insertions(+), 215 deletions(-) diff --git a/docker/models.go b/docker/models.go index 9f41f5c..0feebdc 100644 --- a/docker/models.go +++ b/docker/models.go @@ -12,7 +12,7 @@ const nbImageParts = 2 type ServiceStatus string -// docker engine service status +// Docker engine service status const ( Pending ServiceStatus = "pending" Restarting ServiceStatus = "preparing" @@ -80,146 +80,19 @@ type ServicePort struct { Published int `json:"PublishedPort"` } -type serviceCommandInput struct { - States []serviceState `json:"services"` - Details []serviceDetails `json:"details"` -} - -type Service struct { - ID string - App string - Name string - Image struct { - Name string - Tag string - } - Replicas []Replicas - Ports []ServicePort -} - -func (s *Service) UnmarshalJSON(data []byte) error { - ci := serviceCommandInput{} - if err := json.Unmarshal(data, &ci); err != nil { - return err - } - - if len(ci.Details) == 0 { - return fmt.Errorf("service details must no be empty") - } - - nbReplicas := ci.Details[0].Spec.Mode.Replicated.Replicas - if len(ci.States) < nbReplicas { - return fmt.Errorf("must have %d replicas but have %d", nbReplicas, len(ci.States)) - } - - replicas := getReplicas(ci.States, nbReplicas) - - imageName := ci.Details[0].Spec.Labels.Image - imageNameParts := strings.Split(imageName, ":") - if len(imageNameParts) == nbImageParts { - imageName = imageNameParts[0] - } - - appName := ci.Details[0].Spec.Labels.Namespace - - *s = Service{ - ID: ci.Details[0].ID, - App: appName, - Name: strings.TrimPrefix(ci.Details[0].Spec.Name, appName+"_"), - Image: struct { - Name string - Tag string - }{ - Name: imageName, - Tag: imageNameParts[1], - }, - Ports: ci.Details[0].Endpoint.Ports, - Replicas: replicas, - } - - return nil -} - -func (s Service) String() string { - data := []string{} - fl := fmt.Sprintf( - "%-20s | %-20s | %-30s | %-10s | %-5d", - s.App, - s.Name, - s.Image.Name, - s.Image.Tag, - len(s.Replicas), - ) - data = append(data, fl) - - for idx := range s.Replicas { - data = append(data, fmt.Sprintf( - "%-20s \\ %-20d | %-30s | %-30s", - "", - s.Replicas[idx].Pos, - s.Replicas[idx].State, - s.Replicas[idx].Error, - )) - } - - return strings.Join(data, "\n") -} - -type Services []Service - -func (s Services) GetIDs() []string { - ids := []string{} - for idx := range s { - ids = append(ids, s[idx].ID) - } - return ids -} - -func (s Services) String() string { - data := []string{} - header := fmt.Sprintf( - "%-20s | %-20s | %-30s | %-10s | %-5s", - "Service name", - "Name", - "Image name", - "Image Tag", - "Replicas", - ) - subheader := fmt.Sprintf( - "%-20s \\ %-20s | %-30s | %-30s", - "", - "Replicas", - "Status", - "Error", - ) - hr := "" - for i := 0; i < len(header); i++ { - hr += "*" - } - subhr := "" - for i := 0; i < len(header); i++ { - subhr += "-" - } - data = append(data, hr) - data = append(data, header) - data = append(data, subheader) - data = append(data, hr) - for idx := range s { - data = append(data, s[idx].String()) - data = append(data, subhr) - } - return strings.Join(data, "\n") -} - type serviceDetails struct { ID string `json:"ID"` CreateAt time.Time `json:"CreatedAt"` UpdatedAt time.Time `json:"UpdatedAt"` - Spec struct { - Name string `json:"Name"` + Spec struct { //nolint:govet // fieldalignment + Name string `json:"Name"` + TaskTemplate struct { + Networks []struct { + ID string `json:"Target"` + } `json:"Networks"` + } `json:"TaskTemplate"` Labels struct { Image string `json:"com.docker.stack.image"` - Tag string Namespace string `json:"com.docker.stack.namespace"` } `json:"Labels"` Mode struct { @@ -266,3 +139,80 @@ func (st *serviceState) UnmarshalJSON(data []byte) error { *st = (serviceState)(state) return nil } + +type serviceCommandInput struct { + States []serviceState `json:"services"` + Details []serviceDetails `json:"details"` +} + +type Service struct { + ID string + App string + Name string + Image struct { + Name string + Tag string + } + Networks []string + Replicas []Replicas + Ports []ServicePort +} + +func (s *Service) UnmarshalJSON(data []byte) error { + ci := serviceCommandInput{} + if err := json.Unmarshal(data, &ci); err != nil { + return err + } + + if len(ci.Details) == 0 { + return fmt.Errorf("service details must no be empty") + } + + nbReplicas := ci.Details[0].Spec.Mode.Replicated.Replicas + if len(ci.States) < nbReplicas { + return fmt.Errorf("must have %d replicas but have %d", nbReplicas, len(ci.States)) + } + + networks := []string{} + for idx := range ci.Details[0].Spec.TaskTemplate.Networks { + networks = append(networks, ci.Details[0].Spec.TaskTemplate.Networks[idx].ID) + } + + replicas := getReplicas(ci.States, nbReplicas) + + imageName := ci.Details[0].Spec.Labels.Image + imageNameParts := strings.Split(imageName, ":") + if len(imageNameParts) == nbImageParts { + imageName = imageNameParts[0] + } + + appName := ci.Details[0].Spec.Labels.Namespace + + *s = Service{ + ID: ci.Details[0].ID, + App: appName, + Name: strings.TrimPrefix(ci.Details[0].Spec.Name, appName+"_"), + Image: struct { + Name string + Tag string + }{ + Name: imageName, + Tag: imageNameParts[1], + }, + Networks: networks, + Ports: ci.Details[0].Endpoint.Ports, + Replicas: replicas, + } + + return nil +} + +type Services []Service + +func (s Services) GetIDs() []string { + ids := []string{} + for idx := range s { + ids = append(ids, s[idx].ID) + } + return ids +} diff --git a/main.go b/main.go index 4ef7a62..f2b190d 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "os" "os/signal" "path" + "strconv" "strings" "sync" @@ -240,6 +241,7 @@ func initDeployers( return deps, nil } +//nolint:funlen,mnd // TODO(rmanach): could be splitted func getSwarmServicesDetails(hm *models.HMMap) error { swarmNet := hm.GetSwarmNetInfo() if swarmNet == nil { @@ -260,35 +262,26 @@ func getSwarmServicesDetails(hm *models.HMMap) error { utils.WithColSeparator(" | "), utils.WithHeaderBorderStyle("*"), utils.WithRowSeparator("-"), - utils.WithHeader("App", 20), - utils.WithHeader("Name", 20), - utils.WithHeader("Image", 20), - utils.WithHeader("Tag", 20), - utils.WithHeader("Target-Published", 40), - utils.WithHeader("Replicas", 2), + utils.WithHeader("App", 15), + utils.WithHeader("Name", 15), + utils.WithHeader("Image", 25), + utils.WithHeader("Tag", 10), + utils.WithHeader("Target->Published", 30), + utils.WithHeader("Networks", 20), + utils.WithHeader("Replicas", 10), utils.WithHeader("Status", 10), + utils.WithHeader("Error", 20), ) for idx := range services { columns := []utils.Column{} columns = append( columns, - utils.Column{ - Name: "app", - Value: services[idx].App, - }, - utils.Column{ - Name: "name", - Value: services[idx].Name, - }, - utils.Column{ - Name: "image", - Value: services[idx].Image.Name, - }, - utils.Column{ - Name: "tag", - Value: services[idx].Image.Tag, - }, + utils.NewColumn("app", services[idx].App), + utils.NewColumn("name", services[idx].Name), + utils.NewColumn("image", services[idx].Image.Name), + utils.NewColumn("tag", services[idx].Image.Tag), + utils.NewColumn("networks", strings.Join(services[idx].Networks, ", ")), ) ports := []string{} @@ -296,22 +289,45 @@ func getSwarmServicesDetails(hm *models.HMMap) error { ports = append( ports, fmt.Sprintf( - "%d-%d", + "%d->%d", services[idx].Ports[idy].Target, services[idx].Ports[idy].Published, ), ) } - columns = append(columns, utils.Column{ - Name: "target-published", - Value: strings.Join(ports, ","), - }) + columns = append(columns, utils.NewColumn("target->published", strings.Join(ports, ", "))) - tb.AddRow(columns...) + colSubLines := []utils.Column{} + for idy := range services[idx].Replicas { + nbCol := utils.NewColumn("replicas", strconv.Itoa(services[idx].Replicas[idy].Pos)) + statusCol := utils.NewColumn("status", string(services[idx].Replicas[idy].State)) + errorCol := utils.NewColumn("error", services[idx].Replicas[idy].Error) + + if idy == 0 { + columns = append(columns, nbCol, statusCol, errorCol) + continue + } + + colSubLines = append(colSubLines, nbCol, statusCol, errorCol) + } + + mainRow, err := tb.AddRow(columns...) + if err != nil { + return err + } + + subRow, err := tb.AddRow(colSubLines...) + if err != nil { + return err + } + + if len(colSubLines) > 0 { + mainRow.AddNext(subRow) + } } - tb.Render() + tb.Render() return nil } diff --git a/utils/utils.go b/utils/utils.go index 99b3dc6..a21599b 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -121,67 +121,80 @@ func CatchAndConvertNumber(value string) (int, error) { return strconv.Atoi(string(buf)) } -type HeaderColumn struct { - Name string - Length int +type headerColumn struct { + name string + length int pos int } -type Header struct { - BorderStyle string - headerMeta map[string]*HeaderColumn - Columns []HeaderColumn +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 + t.header.borderStyle = style } } func WithRowSeparator(separator string) TableOption { return func(t *Table) { - t.RowSeparator = separator + t.rowSeparator = separator } } func WithColSeparator(separator string) TableOption { return func(t *Table) { - t.ColSeparator = separator + t.colSeparator = separator } } func WithHeader(name string, length int) TableOption { return func(t *Table) { - if t.Header.Columns == nil { - t.Header.Columns = []HeaderColumn{} + 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}, + 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{} + if t.header.headerMeta == nil { + t.header.headerMeta = map[string]*headerColumn{} } - t.Header.headerMeta[strings.ToLower(name)] = &t.Header.Columns[pos] + t.header.headerMeta[strings.ToLower(name)] = &t.header.columns[pos] } } type Column struct { - Name string - Value string + name string + value string +} + +func NewColumn(name, value string) Column { + return Column{name: name, value: value} +} + +type Row struct { + next *Row // useful for grouping rows + columns []Column +} + +func (r *Row) AddNext(row *Row) { + r.next = row } type Table struct { - RowSeparator string - ColSeparator string - data [][]string - Header Header + rowSeparator string + colSeparator string + rows []*Row + header header cursor int } @@ -191,40 +204,46 @@ func NewTable(options ...TableOption) Table { o(&table) } - table.data = [][]string{} + table.rows = []*Row{} 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) +func (t *Table) AddRow(columns ...Column) (*Row, error) { + if len(columns) == 0 { + return &Row{}, nil } - rowData := make([]string, maxNbCols) - t.data = append(t.data, rowData) + maxNbCols := len(t.header.columns) + if len(columns) > maxNbCols { + return nil, fmt.Errorf("invalid column number, should be %d", maxNbCols) + } + + row := &Row{columns: make([]Column, maxNbCols)} + t.rows = append(t.rows, row) for idx := range columns { - header, ok := t.Header.headerMeta[strings.ToLower(columns[idx].Name)] + header, ok := t.header.headerMeta[strings.ToLower(columns[idx].name)] if !ok { - return fmt.Errorf("no corresponding %s column exist", columns[idx].Name) + return nil, fmt.Errorf("no corresponding %s column exist", columns[idx].name) } - value := columns[idx].Value - if len(value) > header.Length { + value := columns[idx].value + if len(value) > header.length { log.Debug(). - Str("column", columns[idx].Name). + Str("column", columns[idx].name). Str("value", value). Msg("col value too long, trimming...") - value = value[:header.Length-3] + "..." + value = value[:header.length-3] + "..." } - t.data[t.cursor][header.pos] = value + row.columns[header.pos].value = value + row.columns[header.pos].name = columns[idx].name + } t.cursor++ - return nil + return row, nil } func (t *Table) Render() { @@ -232,39 +251,43 @@ func (t *Table) Render() { // header headerParts := []string{} - for idx := range t.Header.Columns { + for idx := range t.header.columns { headerParts = append( headerParts, - fmt.Sprintf("%-*s", t.Header.Columns[idx].Length, t.Header.Columns[idx].Name), + fmt.Sprintf("%-*s", t.header.columns[idx].length, t.header.columns[idx].name), ) } - header := strings.Join(headerParts, t.ColSeparator) + header := strings.Join(headerParts, t.colSeparator) border := "" for i := 0; i < len(header); i++ { - border += t.Header.BorderStyle + border += t.header.borderStyle } rowHr := "" for i := 0; i < len(header); i++ { - rowHr += t.RowSeparator + rowHr += t.rowSeparator } table = append(table, border, header, border) // data - for idx := range t.data { + for idx := range t.rows { lineParts := []string{} - for idy := range t.data[idx] { + for idy := range t.rows[idx].columns { lineParts = append(lineParts, fmt.Sprintf( "%-*s", - t.Header.Columns[idy].Length, - t.data[idx][idy]), + t.header.columns[idy].length, + t.rows[idx].columns[idy].value), ) } - if idx+1 < len(t.data) { - table = append(table, strings.Join(lineParts, t.ColSeparator), rowHr) + if idx+1 < len(t.rows) { + if t.rows[idx].next != nil { + table = append(table, strings.Join(lineParts, t.colSeparator)) + } else { + table = append(table, strings.Join(lineParts, t.colSeparator), rowHr) + } } }