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	"os"
 11	"path/filepath"
 12	"strings"
 13
 14	"github.com/povsister/scp"
 15	"github.com/samber/oops"
 16	pluginLib "gitroot.dev/libs/golang/plugin/model"
 17	"gitroot.dev/server/configuration"
 18	"gitroot.dev/server/logger"
 19	"golang.org/x/crypto/ssh"
 20)
 21
 22type Ssh struct {
 23	currentHost int
 24	logger      *logger.Logger
 25	conf        configuration.SshConf
 26	userManager needUser
 27}
 28
 29func NewSsh(log *logger.Logger, conf configuration.SshConf, userManager needUser) *Ssh {
 30	return &Ssh{
 31		currentHost: 0,
 32		logger:      log.NewSubLogger("ssh"),
 33		conf:        conf,
 34		userManager: userManager,
 35	}
 36}
 37
 38func (e *Ssh) name() string {
 39	return "ssh"
 40}
 41
 42func (e *Ssh) prepareTempDir(dir string) error {
 43	if len(e.conf.Hosts) == 0 {
 44		return oops.Errorf("no host")
 45	}
 46	if err := prepareJobRunner(dir); err != nil {
 47		return oops.Wrapf(err, "can't prepareJobRunner")
 48	}
 49	return nil
 50}
 51
 52func (e *Ssh) exec(dir string, exe pluginLib.Exec, projectPluginCacheDir string) (*pluginLib.ExecStatus, error) {
 53	//TODO manage cache ! how ?
 54
 55	host := e.conf.Hosts[e.currentHost]
 56	e.currentHost++
 57	if e.currentHost >= len(e.conf.Hosts) {
 58		e.currentHost = 0
 59	}
 60
 61	signer := e.userManager.RootCommiter().Signer.Signer()
 62	auth := []ssh.AuthMethod{
 63		ssh.PublicKeys(signer),
 64	}
 65
 66	serverKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(host.PublicKey))
 67	if len(e.conf.Hosts) == 0 {
 68		return nil, oops.Wrapf(err, "can't parse server public key")
 69	}
 70
 71	config := &ssh.ClientConfig{
 72		User:            host.User,
 73		Auth:            auth,
 74		HostKeyCallback: ssh.FixedHostKey(serverKey),
 75	}
 76
 77	addr := fmt.Sprintf("%s:%d", host.Address, host.Port)
 78	e.logger.Info("connexion SSH", logger.NewLoggerPair("user", host.User), logger.NewLoggerPair("addr", addr))
 79
 80	conn, err := ssh.Dial("tcp", addr, config)
 81	if err != nil {
 82		return nil, oops.With("user", host.User, "addr", addr).Wrapf(err, "can't connect")
 83	}
 84	defer conn.Close()
 85
 86	e.logger.Info("scp dir", logger.NewLoggerPair("dir", dir))
 87	scpClient, err := scp.NewClientFromExistingSSH(conn, &scp.ClientOption{})
 88	if err != nil {
 89		return nil, oops.Wrapf(err, "can't open scp conn")
 90	}
 91	defer scpClient.Close()
 92
 93	err = scpClient.CopyDirToRemote(dir, dir, &scp.DirTransferOption{})
 94	if err != nil {
 95		return nil, oops.Wrapf(err, "can't scp dir")
 96	}
 97
 98	execs, err := cmdToExec(exe, true)
 99	if err != nil {
100		return nil, oops.Wrapf(err, "cmdToExec")
101	}
102
103	var logs bytes.Buffer
104
105	session, err := conn.NewSession()
106	if err != nil {
107		return nil, oops.Wrapf(err, "can't open ssh conn")
108	}
109
110	for _, env := range exe.Env {
111		keyVal := strings.Split(env, "=")
112		if len(keyVal) >= 2 {
113			session.Setenv(keyVal[0], keyVal[1])
114		}
115	}
116	session.Stdout = &logs
117	session.Stderr = &logs
118
119	cmdToRun := fmt.Sprintf("cd %s/%s && chmod +x executor && cd ../%s && %s", dir, JOB_CONTEXT_DIR, APP_DIR, strings.Join(execs, " "))
120	err = session.Run(cmdToRun)
121	if err != nil {
122		return nil, oops.Wrapf(err, "can't run over ssh")
123	}
124
125	logStr := logs.Bytes()
126	status, err := parseJobLog(logStr)
127	if err != nil {
128		return nil, oops.Wrapf(err, "can't parse json %s", logStr)
129	}
130
131	logsDir := filepath.Join(dir, JOB_CONTEXT_DIR, LOGS_DIR)
132	os.MkdirAll(logsDir, os.ModePerm)
133	err = scpClient.CopyDirFromRemote(logsDir, logsDir, &scp.DirTransferOption{})
134	if err != nil {
135		return nil, oops.Wrapf(err, "can't scp logs dir to local")
136	}
137
138	artifactsDir := filepath.Join(dir, JOB_CONTEXT_DIR, ARTIFACTS_DIR)
139	os.MkdirAll(artifactsDir, os.ModePerm)
140	err = scpClient.CopyDirFromRemote(artifactsDir, artifactsDir, &scp.DirTransferOption{})
141	if err != nil {
142		return nil, oops.Wrapf(err, "can't scp artifacts dir to local")
143	}
144
145	return status, nil
146}