add a command to get services details on swarm
This commit is contained in:
parent
d90048a69d
commit
b9d594ea37
@ -158,5 +158,5 @@ func (c *SSHConn) Execute(cmd string) (string, error) {
|
|||||||
return "", fmt.Errorf("%w, addr=%s, cmd=%s, err=%v", ErrSSHExecute, c.addr, cmd, err)
|
return "", fmt.Errorf("%w, addr=%s, cmd=%s, err=%v", ErrSSHExecute, c.addr, cmd, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
275
docker/client.go
275
docker/client.go
@ -6,30 +6,250 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.thegux.fr/hmdeploy/connection"
|
||||||
|
"gitea.thegux.fr/hmdeploy/models"
|
||||||
|
"gitea.thegux.fr/hmdeploy/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
appPartsLength = 2
|
||||||
|
imagePartsLength = 2
|
||||||
|
portPartsLength = 2
|
||||||
|
portOutPartsLength = 2
|
||||||
|
replicasPartLength = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDockerClientSave = errors.New("unable to save image into tar")
|
||||||
|
|
||||||
|
ErrDockerClientExtractServicesInputLength = errors.New("bad input length")
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
bufLine = bufLine[:0]
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bufLine = append(bufLine, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
type IClient interface {
|
type IClient interface {
|
||||||
Save(imageName, dest string) (string, error)
|
Save(imageName, dest string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrDockerClientSave = errors.New("unable to save image into tar")
|
// LocalClient is a simple Docker client wrapping the local Docker daemon.
|
||||||
|
|
||||||
// Client is a simple Docker client wrapping the local Docker daemon.
|
|
||||||
// It does not use the Docker API but instead shell command and collect the output.
|
// It does not use the Docker API but instead shell command and collect the output.
|
||||||
//
|
//
|
||||||
// NOTE: for now, it's ok, it only needs one command so, no need to add a fat dedicated
|
// NOTE: for now, it's ok, it only needs one command so, no need to add a fat dedicated
|
||||||
// library with full Docker client API.
|
// library with full Docker client API.
|
||||||
type Client struct{}
|
type LocalClient struct{}
|
||||||
|
|
||||||
var _ IClient = (*Client)(nil)
|
var _ IClient = (*LocalClient)(nil)
|
||||||
|
|
||||||
func NewClient() Client {
|
func NewLocalClient() LocalClient {
|
||||||
return Client{}
|
return LocalClient{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the `imageName` (tag included) in tar format in the target directory: `dest`.
|
// Save saves the `imageName` (tag included) in tar format in the target directory: `dest`.
|
||||||
// The `dest` directory must exist with correct permissions.
|
// The `dest` directory must exist with correct permissions.
|
||||||
func (c *Client) Save(imageName, dest string) (string, error) {
|
func (c *LocalClient) Save(imageName, dest string) (string, error) {
|
||||||
destInfo, err := os.Stat(dest)
|
destInfo, err := os.Stat(dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to stat file, dir=%s, err=%v", dest, err)
|
return "", fmt.Errorf("unable to stat file, dir=%s, err=%v", dest, err)
|
||||||
@ -55,3 +275,42 @@ func (c *Client) Save(imageName, dest string) (string, error) {
|
|||||||
|
|
||||||
return filepath.Join(dest, tarFile), nil
|
return filepath.Join(dest, tarFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteClient is a simple Docker client for remote daemon.
|
||||||
|
// It does not use the Docker API but instead shell command over an SSH connection and collect the output.
|
||||||
|
//
|
||||||
|
// NOTE: for now, it's ok, it only needs one command so, no need to add a fat dedicated
|
||||||
|
// library with full Docker client API.
|
||||||
|
type RemoteClient struct {
|
||||||
|
conn connection.SSHConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoteClient(netInfo *models.HMNetInfo) (RemoteClient, error) {
|
||||||
|
var rc RemoteClient
|
||||||
|
conn, err := connection.NewSSHConn(
|
||||||
|
netInfo.IP.String(),
|
||||||
|
netInfo.SSH.User,
|
||||||
|
netInfo.SSH.Port,
|
||||||
|
netInfo.SSH.PrivKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.conn = conn
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) ExtractServicesDetails() (Services, error) {
|
||||||
|
output, err := c.conn.Execute("docker service ls")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := parseServiceDetails(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|||||||
34
main.go
34
main.go
@ -177,7 +177,6 @@ func loadHMMap() (models.HMMap, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("conf", hmmap_path).Msg("hmmap load successfully")
|
|
||||||
return hmmap, nil
|
return hmmap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +204,7 @@ func initDeployers(
|
|||||||
return deps, fmt.Errorf("%w, swarm net info does not exist", ErrNetInfoNotFound)
|
return deps, fmt.Errorf("%w, swarm net info does not exist", ErrNetInfoNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
dcli := docker.NewClient()
|
dcli := docker.NewLocalClient()
|
||||||
sd, err := deployers.NewSwarmDeployer(ctx, project, swarmNet, &dcli)
|
sd, err := deployers.NewSwarmDeployer(ctx, project, swarmNet, &dcli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deps, fmt.Errorf("%w, unable to init swarm deployer, err=%v", ErrDeployerInit, err)
|
return deps, fmt.Errorf("%w, unable to init swarm deployer, err=%v", ErrDeployerInit, err)
|
||||||
@ -240,7 +239,27 @@ func initDeployers(
|
|||||||
return deps, nil
|
return deps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() { //nolint: all //hjf
|
func getSwarmServicesDetails(hm *models.HMMap) error {
|
||||||
|
swarmNet := hm.GetSwarmNetInfo()
|
||||||
|
if swarmNet == nil {
|
||||||
|
return fmt.Errorf("%w, swarm net info does not exist", ErrNetInfoNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := docker.NewRemoteClient(swarmNet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := cli.ExtractServicesDetails()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(services)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() { //nolint: funlen //TODO: to reduce
|
||||||
ctx, fnCancel := signal.NotifyContext(
|
ctx, fnCancel := signal.NotifyContext(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
os.Interrupt,
|
os.Interrupt,
|
||||||
@ -251,6 +270,7 @@ func main() { //nolint: all //hjf
|
|||||||
destroy := flag.Bool("destroy", false, "delete the deployed project")
|
destroy := flag.Bool("destroy", false, "delete the deployed project")
|
||||||
noNginx := flag.Bool("no-nginx", false, "no Nginx deployment")
|
noNginx := flag.Bool("no-nginx", false, "no Nginx deployment")
|
||||||
debug := flag.Bool("debug", false, "show debug logs")
|
debug := flag.Bool("debug", false, "show debug logs")
|
||||||
|
details := flag.Bool("details", false, "extract swarm details and return")
|
||||||
confirm := flag.Bool(
|
confirm := flag.Bool(
|
||||||
"confirm",
|
"confirm",
|
||||||
false,
|
false,
|
||||||
@ -259,13 +279,19 @@ func main() { //nolint: all //hjf
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
initLogger(*debug)
|
initLogger(*debug)
|
||||||
log.Info().Msg("hmdeploy started")
|
|
||||||
|
|
||||||
hmmap, err := loadHMMap()
|
hmmap, err := loadHMMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to load conf")
|
log.Fatal().Err(err).Msg("failed to load conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *details {
|
||||||
|
if err = getSwarmServicesDetails(&hmmap); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("unable to extract swarm services details")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project, err := models.ProjectFromDir(*projectDir)
|
project, err := models.ProjectFromDir(*projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Str("dir", *projectDir).Err(err).Msg("unable to init project from directory")
|
log.Fatal().Str("dir", *projectDir).Err(err).Msg("unable to init project from directory")
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -98,3 +99,24 @@ func Confirm(ctx context.Context, destroy bool) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatchAndConvertNumber finds and converts the first consecutives digits in integer.
|
||||||
|
func CatchAndConvertNumber(value string) (int, error) {
|
||||||
|
buf := []rune{}
|
||||||
|
|
||||||
|
hasLeftTrimmed := false
|
||||||
|
for _, c := range value {
|
||||||
|
if c >= '0' && c <= '9' {
|
||||||
|
hasLeftTrimmed = true
|
||||||
|
buf = append(buf, c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// early return if no number catch
|
||||||
|
if hasLeftTrimmed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(string(buf))
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user