package docker import ( "encoding/json" "fmt" "strconv" "strings" "time" "github.com/rs/zerolog/log" ) 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 serviceDetails struct { ID string `json:"ID"` CreateAt time.Time `json:"CreatedAt"` UpdatedAt time.Time `json:"UpdatedAt"` 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"` 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 } 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 { log.Warn().Msg(fmt.Sprintf("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 }