GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package exec
  6
  7import (
  8	"bytes"
  9	"fmt"
 10	"path/filepath"
 11	"strings"
 12
 13	"github.com/povsister/scp"
 14	"github.com/samber/oops"
 15	pluginLib "gitroot.dev/libs/golang/plugin/model"
 16	"gitroot.dev/server/configuration"
 17	"gitroot.dev/server/logger"
 18	"golang.org/x/crypto/ssh"
 19)
 20
 21type Ssh struct {
 22	currentHost int
 23	logger      *logger.Logger
 24	conf        configuration.SshConf
 25	userManager needUser
 26}
 27
 28func NewSsh(log *logger.Logger, conf configuration.SshConf, userManager needUser) *Ssh {
 29	return &Ssh{
 30		currentHost: 0,
 31		logger:      log.NewSubLogger("ssh"),
 32		conf:        conf,
 33		userManager: userManager,
 34	}
 35}
 36
 37func (e *Ssh) name() string {
 38	return "ssh"
 39}
 40
 41func (e *Ssh) exec(dir string, exe pluginLib.Exec) (*pluginLib.ExecStatus, error) {
 42	if len(e.conf.Hosts) == 0 {
 43		return nil, oops.Errorf("no host")
 44	}
 45
 46	host := e.conf.Hosts[e.currentHost]
 47	e.currentHost++
 48	if e.currentHost >= len(e.conf.Hosts) {
 49		e.currentHost = 0
 50	}
 51
 52	signer := e.userManager.RootCommiter().Signer.Signer()
 53	auth := []ssh.AuthMethod{
 54		ssh.PublicKeys(signer),
 55	}
 56
 57	serverKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(host.PublicKey))
 58	if len(e.conf.Hosts) == 0 {
 59		return nil, oops.Wrapf(err, "can't parse server public key")
 60	}
 61
 62	config := &ssh.ClientConfig{
 63		User:            host.User,
 64		Auth:            auth,
 65		HostKeyCallback: ssh.FixedHostKey(serverKey),
 66	}
 67
 68	addr := fmt.Sprintf("%s:%d", host.Address, host.Port)
 69	e.logger.Info("connexion SSH", logger.NewLoggerPair("user", host.User), logger.NewLoggerPair("addr", addr))
 70
 71	conn, err := ssh.Dial("tcp", addr, config)
 72	if err != nil {
 73		return nil, oops.With("user", host.User, "addr", addr).Wrapf(err, "can't connect")
 74	}
 75	defer conn.Close()
 76
 77	e.logger.Info("scp dir", logger.NewLoggerPair("dir", dir))
 78	scpClient, err := scp.NewClientFromExistingSSH(conn, &scp.ClientOption{})
 79	if err != nil {
 80		return nil, fmt.Errorf("échec de la création du client SCP: %w", err)
 81	}
 82	defer scpClient.Close()
 83
 84	err = scpClient.CopyDirToRemote(dir, dir, &scp.DirTransferOption{})
 85	if err != nil {
 86		return nil, fmt.Errorf("échec du transfert SCP du répertoire %s: %w", dir, err)
 87	}
 88
 89	status := &pluginLib.ExecStatus{
 90		CmdsExec:   make([]string, len(exe.Cmds)),
 91		CmdsStatus: make([]int, len(exe.Cmds)),
 92		CmdsLogs:   make([]string, len(exe.Cmds)),
 93		CmdsStats:  make([]pluginLib.CmdStats, len(exe.Cmds)),
 94	}
 95
 96	for i, cmd := range exe.Cmds {
 97		var logs bytes.Buffer
 98
 99		session, err := conn.NewSession()
100		if err != nil {
101			return nil, fmt.Errorf("échec de la création de la session SSH: %w", err)
102		}
103
104		for _, env := range exe.Env {
105			keyVal := strings.Split(env, "=")
106			if len(keyVal) >= 2 {
107				session.Setenv(keyVal[0], keyVal[1])
108			}
109		}
110		session.Stdout = &logs
111		session.Stderr = &logs
112		status.CmdsExec[i] = fmt.Sprintf("cd %s && %s %s", filepath.Join(dir, APP_DIR), cmd.Cmd, strings.Join(cmd.Args, " "))
113		e.logger.Warn("exec a cmd", logger.NewLoggerPair("cmd", status.CmdsExec[i]))
114		err = session.Run(status.CmdsExec[i])
115		status.CmdsLogs[i] = logs.String()
116		if err != nil {
117			if exitErr, ok := err.(*ssh.ExitError); ok {
118				status.CmdsStatus[i] = exitErr.ExitStatus()
119			}
120			status.CmdsStatus[i] = -1
121		}
122
123		session.Close()
124	}
125
126	return status, nil
127}