move command struct in separate file
This commit is contained in:
parent
b9d594ea37
commit
5330eac174
2
Makefile
2
Makefile
@ -7,7 +7,7 @@ build: lint
|
||||
@echo "building binary..."
|
||||
@go build -o $(BIN_NAME) main.go && echo "$(BIN_NAME) built"
|
||||
|
||||
install: lint
|
||||
install:
|
||||
@$(shell whereis $(BIN_NAME) | cut -d ' ' -f2 | xargs rm -f)
|
||||
@go install
|
||||
@echo "program installed: $(GOPATH)/bin/hmdeploy"
|
||||
|
||||
237
docker/client.go
237
docker/client.go
@ -1,18 +1,15 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.thegux.fr/hmdeploy/connection"
|
||||
"gitea.thegux.fr/hmdeploy/models"
|
||||
"gitea.thegux.fr/hmdeploy/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,204 +27,19 @@ var (
|
||||
ErrDockerClientExtractServicesParse = errors.New("parse error")
|
||||
)
|
||||
|
||||
type Port struct {
|
||||
In int
|
||||
Out int
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
App string
|
||||
Name string
|
||||
Image struct {
|
||||
Name string
|
||||
Tag string
|
||||
}
|
||||
Ports []Port
|
||||
Replicas int
|
||||
}
|
||||
|
||||
func (s Service) String() string {
|
||||
return fmt.Sprintf(
|
||||
"%-20s | %-20s | %-30s | %-10s | %-5d",
|
||||
s.App,
|
||||
s.Name,
|
||||
s.Image.Name,
|
||||
s.Image.Tag,
|
||||
s.Replicas,
|
||||
)
|
||||
}
|
||||
|
||||
type Services []Service
|
||||
|
||||
func (s Services) String() string {
|
||||
data := []string{}
|
||||
header := fmt.Sprintf(
|
||||
"%-20s | %-20s | %-30s | %-10s | %-5s",
|
||||
"Service name",
|
||||
"App name",
|
||||
"Image name",
|
||||
"Image Tag",
|
||||
"Replicas",
|
||||
)
|
||||
hr := ""
|
||||
for i := 0; i < len(header); i++ {
|
||||
hr += "-"
|
||||
}
|
||||
data = append(data, hr)
|
||||
data = append(data, header)
|
||||
data = append(data, hr)
|
||||
for idx := range s {
|
||||
data = append(data, s[idx].String())
|
||||
}
|
||||
return strings.Join(data, "\n")
|
||||
}
|
||||
|
||||
type serviceDetailsHeader map[string]int
|
||||
|
||||
func (h serviceDetailsHeader) getPos(value string) int {
|
||||
v, ok := h[value]
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// serviceFromString instantiates a new service from the `docker service ls` command output.
|
||||
// TODO: it's ugly... To rework, if i'm not too lazy
|
||||
func serviceFromString( //nolint:all // todo
|
||||
header serviceDetailsHeader,
|
||||
value string,
|
||||
) (Service, error) {
|
||||
var srv Service
|
||||
|
||||
parts := strings.Split(value, " ")
|
||||
parts = slices.DeleteFunc(parts, func(e string) bool { return e == "" })
|
||||
|
||||
if pos := header.getPos("name"); pos != -1 {
|
||||
appParts := strings.Split(parts[pos], "_")
|
||||
if len(appParts) >= appPartsLength {
|
||||
srv.App = appParts[len(appParts)-1]
|
||||
srv.Name = appParts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if pos := header.getPos("image"); pos != -1 {
|
||||
imageParts := strings.Split(parts[pos], ":")
|
||||
if len(imageParts) >= imagePartsLength {
|
||||
srv.Image.Name = imageParts[0]
|
||||
srv.Image.Tag = imageParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if pos := header.getPos("replicas"); pos != -1 {
|
||||
replicasParts := strings.Split(parts[pos], "/")
|
||||
if len(replicasParts) >= replicasPartLength {
|
||||
replicas, err := strconv.Atoi(replicasParts[1])
|
||||
if err != nil {
|
||||
return srv, fmt.Errorf(
|
||||
"%w, replicas conversion failed: %s",
|
||||
ErrDockerClientExtractServicesParse,
|
||||
string(replicasParts[1]),
|
||||
)
|
||||
}
|
||||
srv.Replicas = replicas
|
||||
}
|
||||
}
|
||||
|
||||
srv.Ports = []Port{}
|
||||
|
||||
if pos := header.getPos("ports"); pos != -1 && pos < len(parts) {
|
||||
// ports parts must exceed the header pos so, to find missing ports
|
||||
// the pos difference is computed in order to retrieve them
|
||||
portsParts := []string{}
|
||||
diff := len(parts) - pos
|
||||
for i := 0; i < diff; i++ {
|
||||
portsParts = append(portsParts, parts[pos+i])
|
||||
}
|
||||
|
||||
for _, port := range portsParts {
|
||||
servicePort := Port{}
|
||||
// port in/out are displayed like "out->in"
|
||||
portParts := strings.Split(port, "->")
|
||||
if len(portParts) >= portPartsLength {
|
||||
// out port is displayed like "ip:port"
|
||||
portOutParts := strings.Split(portParts[0], ":")
|
||||
if len(portOutParts) == portOutPartsLength {
|
||||
portOut, err := utils.CatchAndConvertNumber(portOutParts[1])
|
||||
if err != nil {
|
||||
return srv, fmt.Errorf(
|
||||
"%w, out port conversion failed: %s",
|
||||
ErrDockerClientExtractServicesParse,
|
||||
string(portOutParts[1]),
|
||||
)
|
||||
}
|
||||
servicePort.Out = portOut
|
||||
}
|
||||
|
||||
portIn, err := utils.CatchAndConvertNumber(portParts[1])
|
||||
if err != nil {
|
||||
return srv, fmt.Errorf(
|
||||
"%w, in port conversion failed: %s",
|
||||
ErrDockerClientExtractServicesParse,
|
||||
string(portParts[1]),
|
||||
)
|
||||
}
|
||||
servicePort.In = portIn
|
||||
|
||||
srv.Ports = append(srv.Ports, servicePort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Parse the `docker service ls` header (ID NAME MODE REPLICAS IMAGE PORTS)
|
||||
func parseServiceDetailsHeader(value string) serviceDetailsHeader {
|
||||
header := serviceDetailsHeader{}
|
||||
|
||||
parts := strings.Split(value, " ")
|
||||
parts = slices.DeleteFunc(parts, func(e string) bool { return e == "" })
|
||||
|
||||
for idx, part := range parts {
|
||||
header[part] = idx
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func parseServiceDetails(cmdOutput string) (Services, error) {
|
||||
services := Services{}
|
||||
var header serviceDetailsHeader
|
||||
|
||||
func parseIDs(cmdOutput string) []string {
|
||||
ids := []string{}
|
||||
bufLine := []rune{}
|
||||
isHeaderCatched := false
|
||||
for _, c := range cmdOutput {
|
||||
if c == '\n' {
|
||||
if !isHeaderCatched {
|
||||
header = parseServiceDetailsHeader(strings.ToLower(string(bufLine)))
|
||||
bufLine = bufLine[:0]
|
||||
isHeaderCatched = true
|
||||
continue
|
||||
}
|
||||
|
||||
service, err := serviceFromString(header, string(bufLine))
|
||||
if err != nil {
|
||||
return services, fmt.Errorf(
|
||||
"%w, err=%v",
|
||||
ErrDockerClientExtractServicesParse,
|
||||
err,
|
||||
)
|
||||
}
|
||||
services = append(services, service)
|
||||
ids = append(ids, string(bufLine))
|
||||
bufLine = bufLine[:0]
|
||||
|
||||
continue
|
||||
}
|
||||
bufLine = append(bufLine, c)
|
||||
}
|
||||
|
||||
return services, nil
|
||||
return ids
|
||||
}
|
||||
|
||||
type IClient interface {
|
||||
@ -301,16 +113,49 @@ func NewRemoteClient(netInfo *models.HMNetInfo) (RemoteClient, error) {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (c *RemoteClient) ExtractServicesDetails() (Services, error) {
|
||||
output, err := c.conn.Execute("docker service ls")
|
||||
func (c *RemoteClient) getIDS() ([]string, error) {
|
||||
output, err := c.conn.Execute("docker service ls -q")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
services, err := parseServiceDetails(output)
|
||||
return parseIDs(output), nil
|
||||
}
|
||||
|
||||
func (c *RemoteClient) getServiceDetails(id string) (Service, error) {
|
||||
output, err := c.conn.Execute(
|
||||
fmt.Sprintf(
|
||||
`echo "{\"services\": [$(echo $(cmd=$(docker service ps %s --format json); cmd=$(echo $cmd | tr '} {' '},{'); echo $cmd))], \"details\": $(docker service inspect %s --format=json)}"`,
|
||||
id,
|
||||
id,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
|
||||
sc := Service{}
|
||||
if err := json.Unmarshal([]byte(output), &sc); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func (c *RemoteClient) ExtractServicesDetails() (Services, error) {
|
||||
ids, err := c.getIDS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
services := Services{}
|
||||
for _, id := range ids {
|
||||
srv, err := c.getServiceDetails(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
services = append(services, srv)
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
268
docker/models.go
Normal file
268
docker/models.go
Normal file
@ -0,0 +1,268 @@
|
||||
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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user