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..."
|
@echo "building binary..."
|
||||||
@go build -o $(BIN_NAME) main.go && echo "$(BIN_NAME) built"
|
@go build -o $(BIN_NAME) main.go && echo "$(BIN_NAME) built"
|
||||||
|
|
||||||
install: lint
|
install:
|
||||||
@$(shell whereis $(BIN_NAME) | cut -d ' ' -f2 | xargs rm -f)
|
@$(shell whereis $(BIN_NAME) | cut -d ' ' -f2 | xargs rm -f)
|
||||||
@go install
|
@go install
|
||||||
@echo "program installed: $(GOPATH)/bin/hmdeploy"
|
@echo "program installed: $(GOPATH)/bin/hmdeploy"
|
||||||
|
|||||||
237
docker/client.go
237
docker/client.go
@ -1,18 +1,15 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.thegux.fr/hmdeploy/connection"
|
"gitea.thegux.fr/hmdeploy/connection"
|
||||||
"gitea.thegux.fr/hmdeploy/models"
|
"gitea.thegux.fr/hmdeploy/models"
|
||||||
"gitea.thegux.fr/hmdeploy/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -30,204 +27,19 @@ var (
|
|||||||
ErrDockerClientExtractServicesParse = errors.New("parse error")
|
ErrDockerClientExtractServicesParse = errors.New("parse error")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Port struct {
|
func parseIDs(cmdOutput string) []string {
|
||||||
In int
|
ids := []string{}
|
||||||
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
|
|
||||||
|
|
||||||
bufLine := []rune{}
|
bufLine := []rune{}
|
||||||
isHeaderCatched := false
|
|
||||||
for _, c := range cmdOutput {
|
for _, c := range cmdOutput {
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
if !isHeaderCatched {
|
ids = append(ids, string(bufLine))
|
||||||
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)
|
|
||||||
bufLine = bufLine[:0]
|
bufLine = bufLine[:0]
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bufLine = append(bufLine, c)
|
bufLine = append(bufLine, c)
|
||||||
}
|
}
|
||||||
|
return ids
|
||||||
return services, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IClient interface {
|
type IClient interface {
|
||||||
@ -301,16 +113,49 @@ func NewRemoteClient(netInfo *models.HMNetInfo) (RemoteClient, error) {
|
|||||||
return rc, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteClient) ExtractServicesDetails() (Services, error) {
|
func (c *RemoteClient) getIDS() ([]string, error) {
|
||||||
output, err := c.conn.Execute("docker service ls")
|
output, err := c.conn.Execute("docker service ls -q")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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