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}