package docker import ( "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "gitea.thegux.fr/hmdeploy/connection" "gitea.thegux.fr/hmdeploy/models" ) 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") ) func parseIDs(cmdOutput string) []string { ids := []string{} bufLine := []rune{} for _, c := range cmdOutput { if c == '\n' { ids = append(ids, string(bufLine)) bufLine = bufLine[:0] continue } bufLine = append(bufLine, c) } return ids } type IClient interface { Save(imageName, dest string) (string, error) } // LocalClient 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. // // 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 LocalClient struct{} var _ IClient = (*LocalClient)(nil) func NewLocalClient() LocalClient { return LocalClient{} } // Save saves the `imageName` (tag included) in tar format in the target directory: `dest`. // The `dest` directory must exist with correct permissions. func (c *LocalClient) Save(imageName, dest string) (string, error) { destInfo, err := os.Stat(dest) if err != nil { return "", fmt.Errorf("unable to stat file, dir=%s, err=%v", dest, err) } if !destInfo.IsDir() { return "", fmt.Errorf("dest file must be a directory, dir=%s, err=%v", dest, err) } tarFile := fmt.Sprintf("%s.tar", imageName) cmd := exec.Command("docker", "save", "-o", tarFile, imageName) cmd.Dir = dest if _, err := cmd.Output(); err != nil { return "", fmt.Errorf( "%w, dir=%s, image=%s, err=%v", ErrDockerClientSave, dest, imageName, err, ) } 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) getIDS() ([]string, error) { output, err := c.conn.Execute("docker service ls -q") if err != nil { return nil, err } 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 }