269 lines
5.5 KiB
Go
269 lines
5.5 KiB
Go
package docker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const nbImageParts = 2
|
|
|
|
type ServiceStatus string
|
|
|
|
// docker engine service status
|
|
const (
|
|
Pending ServiceStatus = "pending"
|
|
Restarting ServiceStatus = "preparing"
|
|
Starting = "starting"
|
|
Running = "running"
|
|
Failed = "failed"
|
|
Shutdown = "shutdown"
|
|
Rejected = "rejected"
|
|
Orphaned = "orphaned"
|
|
Complete = "complete"
|
|
New = "new"
|
|
Unknown = "unknown"
|
|
)
|
|
|
|
var mapServiceStatus = map[string]ServiceStatus{
|
|
"pending": Pending,
|
|
"restarting": Restarting,
|
|
"starting": Starting,
|
|
"running": Running,
|
|
"failed": Failed,
|
|
"shutdown": Shutdown,
|
|
"rejected": Rejected,
|
|
"orphaned": Orphaned,
|
|
"complete": Complete,
|
|
"new": New,
|
|
}
|
|
|
|
func serviceStatusFromString(value string) ServiceStatus {
|
|
if v, ok := mapServiceStatus[strings.ToLower(value)]; ok {
|
|
return v
|
|
}
|
|
return Unknown
|
|
}
|
|
|
|
func getReplicas(states []serviceState, nbReplicas int) []Replicas {
|
|
replicas := []Replicas{}
|
|
|
|
// collect latest services for each replicas
|
|
rep := 1
|
|
for idx := range states {
|
|
if rep > nbReplicas {
|
|
break
|
|
}
|
|
if states[idx].ReplicasNumber == rep {
|
|
replicas = append(replicas, Replicas{
|
|
Pos: rep,
|
|
State: serviceStatusFromString(states[idx].Current),
|
|
Error: states[idx].Error,
|
|
})
|
|
rep++
|
|
}
|
|
}
|
|
|
|
return replicas
|
|
}
|
|
|
|
type Replicas struct {
|
|
State ServiceStatus
|
|
Error string
|
|
Pos int
|
|
}
|
|
|
|
type ServicePort struct {
|
|
Target int `json:"TargetPort"`
|
|
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: ci.Details[0].Spec.Labels.Tag,
|
|
},
|
|
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"`
|
|
Labels struct {
|
|
Image string `json:"com.docker.stack.image"`
|
|
Tag string
|
|
Namespace string `json:"com.docker.stack.namespace"`
|
|
} `json:"Labels"`
|
|
Mode struct {
|
|
Replicated struct {
|
|
Replicas int `json:"Replicas"`
|
|
} `json:"Replicated"`
|
|
} `json:"Mode"`
|
|
} `json:"spec"`
|
|
Endpoint struct {
|
|
Ports []ServicePort `json:"Ports"`
|
|
} `json:"Endpoint"`
|
|
}
|
|
|
|
type serviceStateInput serviceState
|
|
|
|
type serviceState struct {
|
|
Name string `json:"Name"`
|
|
Current string `json:"CurrentState"`
|
|
Desired string `json:"DesiredState"`
|
|
Error string `json:"Error"`
|
|
ReplicasNumber int
|
|
}
|
|
|
|
func (st *serviceState) UnmarshalJSON(data []byte) error {
|
|
state := serviceStateInput{}
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
return err
|
|
}
|
|
|
|
nameParts := strings.Split(state.Name, ".")
|
|
if len(nameParts) > 0 {
|
|
replicasNumber, err := strconv.Atoi(nameParts[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
state.ReplicasNumber = replicasNumber
|
|
}
|
|
|
|
currentStateParts := strings.Split(state.Current, ",")
|
|
if len(currentStateParts) > 0 {
|
|
state.Current = currentStateParts[0]
|
|
}
|
|
|
|
*st = (serviceState)(state)
|
|
return nil
|
|
}
|