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 }