format table + add extra fields

This commit is contained in:
rmanach 2025-04-29 15:59:57 +02:00
parent 64f7f4c131
commit bfaf6f10c6
3 changed files with 204 additions and 215 deletions

View File

@ -12,7 +12,7 @@ const nbImageParts = 2
type ServiceStatus string type ServiceStatus string
// docker engine service status // Docker engine service status
const ( const (
Pending ServiceStatus = "pending" Pending ServiceStatus = "pending"
Restarting ServiceStatus = "preparing" Restarting ServiceStatus = "preparing"
@ -80,146 +80,19 @@ type ServicePort struct {
Published int `json:"PublishedPort"` 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 { type serviceDetails struct {
ID string `json:"ID"` ID string `json:"ID"`
CreateAt time.Time `json:"CreatedAt"` CreateAt time.Time `json:"CreatedAt"`
UpdatedAt time.Time `json:"UpdatedAt"` UpdatedAt time.Time `json:"UpdatedAt"`
Spec struct { Spec struct { //nolint:govet // fieldalignment
Name string `json:"Name"` Name string `json:"Name"`
TaskTemplate struct {
Networks []struct {
ID string `json:"Target"`
} `json:"Networks"`
} `json:"TaskTemplate"`
Labels struct { Labels struct {
Image string `json:"com.docker.stack.image"` Image string `json:"com.docker.stack.image"`
Tag string
Namespace string `json:"com.docker.stack.namespace"` Namespace string `json:"com.docker.stack.namespace"`
} `json:"Labels"` } `json:"Labels"`
Mode struct { Mode struct {
@ -266,3 +139,80 @@ func (st *serviceState) UnmarshalJSON(data []byte) error {
*st = (serviceState)(state) *st = (serviceState)(state)
return nil 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
}

74
main.go
View File

@ -9,6 +9,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path" "path"
"strconv"
"strings" "strings"
"sync" "sync"
@ -240,6 +241,7 @@ func initDeployers(
return deps, nil return deps, nil
} }
//nolint:funlen,mnd // TODO(rmanach): could be splitted
func getSwarmServicesDetails(hm *models.HMMap) error { func getSwarmServicesDetails(hm *models.HMMap) error {
swarmNet := hm.GetSwarmNetInfo() swarmNet := hm.GetSwarmNetInfo()
if swarmNet == nil { if swarmNet == nil {
@ -260,35 +262,26 @@ func getSwarmServicesDetails(hm *models.HMMap) error {
utils.WithColSeparator(" | "), utils.WithColSeparator(" | "),
utils.WithHeaderBorderStyle("*"), utils.WithHeaderBorderStyle("*"),
utils.WithRowSeparator("-"), utils.WithRowSeparator("-"),
utils.WithHeader("App", 20), utils.WithHeader("App", 15),
utils.WithHeader("Name", 20), utils.WithHeader("Name", 15),
utils.WithHeader("Image", 20), utils.WithHeader("Image", 25),
utils.WithHeader("Tag", 20), utils.WithHeader("Tag", 10),
utils.WithHeader("Target-Published", 40), utils.WithHeader("Target->Published", 30),
utils.WithHeader("Replicas", 2), utils.WithHeader("Networks", 20),
utils.WithHeader("Replicas", 10),
utils.WithHeader("Status", 10), utils.WithHeader("Status", 10),
utils.WithHeader("Error", 20),
) )
for idx := range services { for idx := range services {
columns := []utils.Column{} columns := []utils.Column{}
columns = append( columns = append(
columns, columns,
utils.Column{ utils.NewColumn("app", services[idx].App),
Name: "app", utils.NewColumn("name", services[idx].Name),
Value: services[idx].App, utils.NewColumn("image", services[idx].Image.Name),
}, utils.NewColumn("tag", services[idx].Image.Tag),
utils.Column{ utils.NewColumn("networks", strings.Join(services[idx].Networks, ", ")),
Name: "name",
Value: services[idx].Name,
},
utils.Column{
Name: "image",
Value: services[idx].Image.Name,
},
utils.Column{
Name: "tag",
Value: services[idx].Image.Tag,
},
) )
ports := []string{} ports := []string{}
@ -296,22 +289,45 @@ func getSwarmServicesDetails(hm *models.HMMap) error {
ports = append( ports = append(
ports, ports,
fmt.Sprintf( fmt.Sprintf(
"%d-%d", "%d->%d",
services[idx].Ports[idy].Target, services[idx].Ports[idy].Target,
services[idx].Ports[idy].Published, services[idx].Ports[idy].Published,
), ),
) )
} }
columns = append(columns, utils.Column{ columns = append(columns, utils.NewColumn("target->published", strings.Join(ports, ", ")))
Name: "target-published",
Value: 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 return nil
} }

View File

@ -121,67 +121,80 @@ func CatchAndConvertNumber(value string) (int, error) {
return strconv.Atoi(string(buf)) return strconv.Atoi(string(buf))
} }
type HeaderColumn struct { type headerColumn struct {
Name string name string
Length int length int
pos int pos int
} }
type Header struct { type header struct {
BorderStyle string borderStyle string
headerMeta map[string]*HeaderColumn headerMeta map[string]*headerColumn
Columns []HeaderColumn columns []headerColumn
} }
type TableOption func(*Table) type TableOption func(*Table)
func WithHeaderBorderStyle(style string) TableOption { func WithHeaderBorderStyle(style string) TableOption {
return func(t *Table) { return func(t *Table) {
t.Header.BorderStyle = style t.header.borderStyle = style
} }
} }
func WithRowSeparator(separator string) TableOption { func WithRowSeparator(separator string) TableOption {
return func(t *Table) { return func(t *Table) {
t.RowSeparator = separator t.rowSeparator = separator
} }
} }
func WithColSeparator(separator string) TableOption { func WithColSeparator(separator string) TableOption {
return func(t *Table) { return func(t *Table) {
t.ColSeparator = separator t.colSeparator = separator
} }
} }
func WithHeader(name string, length int) TableOption { func WithHeader(name string, length int) TableOption {
return func(t *Table) { return func(t *Table) {
if t.Header.Columns == nil { if t.header.columns == nil {
t.Header.Columns = []HeaderColumn{} t.header.columns = []headerColumn{}
} }
pos := len(t.Header.Columns) pos := len(t.header.columns)
t.Header.Columns = append( t.header.columns = append(
t.Header.Columns, t.header.columns,
HeaderColumn{Name: name, Length: length, pos: pos}, headerColumn{name: name, length: length, pos: pos},
) )
if t.Header.headerMeta == nil { if t.header.headerMeta == nil {
t.Header.headerMeta = map[string]*HeaderColumn{} 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 { type Column struct {
Name string name string
Value 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 { type Table struct {
RowSeparator string rowSeparator string
ColSeparator string colSeparator string
data [][]string rows []*Row
Header Header header header
cursor int cursor int
} }
@ -191,40 +204,46 @@ func NewTable(options ...TableOption) Table {
o(&table) o(&table)
} }
table.data = [][]string{} table.rows = []*Row{}
return table return table
} }
func (t *Table) AddRow(columns ...Column) error { func (t *Table) AddRow(columns ...Column) (*Row, error) {
maxNbCols := len(t.Header.Columns) if len(columns) == 0 {
if len(columns) > maxNbCols { return &Row{}, nil
return fmt.Errorf("invalid column number, should be %d", maxNbCols)
} }
rowData := make([]string, maxNbCols) maxNbCols := len(t.header.columns)
t.data = append(t.data, rowData) 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 { 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 { 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 value := columns[idx].value
if len(value) > header.Length { if len(value) > header.length {
log.Debug(). log.Debug().
Str("column", columns[idx].Name). Str("column", columns[idx].name).
Str("value", value). Str("value", value).
Msg("col value too long, trimming...") 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++ t.cursor++
return nil return row, nil
} }
func (t *Table) Render() { func (t *Table) Render() {
@ -232,39 +251,43 @@ func (t *Table) Render() {
// header // header
headerParts := []string{} headerParts := []string{}
for idx := range t.Header.Columns { for idx := range t.header.columns {
headerParts = append( headerParts = append(
headerParts, 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 := "" border := ""
for i := 0; i < len(header); i++ { for i := 0; i < len(header); i++ {
border += t.Header.BorderStyle border += t.header.borderStyle
} }
rowHr := "" rowHr := ""
for i := 0; i < len(header); i++ { for i := 0; i < len(header); i++ {
rowHr += t.RowSeparator rowHr += t.rowSeparator
} }
table = append(table, border, header, border) table = append(table, border, header, border)
// data // data
for idx := range t.data { for idx := range t.rows {
lineParts := []string{} lineParts := []string{}
for idy := range t.data[idx] { for idy := range t.rows[idx].columns {
lineParts = append(lineParts, fmt.Sprintf( lineParts = append(lineParts, fmt.Sprintf(
"%-*s", "%-*s",
t.Header.Columns[idy].Length, t.header.columns[idy].length,
t.data[idx][idy]), t.rows[idx].columns[idy].value),
) )
} }
if idx+1 < len(t.data) { if idx+1 < len(t.rows) {
table = append(table, strings.Join(lineParts, t.ColSeparator), rowHr) if t.rows[idx].next != nil {
table = append(table, strings.Join(lineParts, t.colSeparator))
} else {
table = append(table, strings.Join(lineParts, t.colSeparator), rowHr)
}
} }
} }