hmdeploy/deployers/commons.go
2025-10-29 09:03:38 +01:00

229 lines
5.2 KiB
Go

package deployers
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sync/atomic"
"time"
"gitea.sonak.fr/hmdeploy/connection"
"gitea.sonak.fr/hmdeploy/models"
"github.com/rs/zerolog/log"
)
var (
ErrContextDone = errors.New("unable to execute, context done")
ErrEmptyArchive = errors.New("no file to add to the archive")
)
type IDeployer interface {
Type() DeployerType
Deploy() error
Build() error
Destroy() error
Clear() error
Error() error
Done() <-chan struct{}
}
type DeployerType string
const (
Nginx DeployerType = "nginx"
Swarm DeployerType = "swarm"
GracefulTimeout = 10 * time.Second
)
// Base struct of the deployers.
// It handles the main informations to build a deployer.
//
// "Inherited" deployers must implement three methods in order
// to satify the `IDeployer` contract:
// - `Deploy() error`: run shell command to deploy the archive remotly
// - `Build() error`: build the archive
// - `Clear() error`: clean all the ressources locally and remotly
type deployer struct { //nolint:govet // ll
ctx context.Context
fnCancel context.CancelFunc
chDone chan struct{}
processing atomic.Bool
type_ DeployerType
errFlag error
conn connection.IConnection
project *models.Project
archivePath string
deploymentDir string
}
func newDeployer(
ctx context.Context,
type_ DeployerType,
project *models.Project,
netInfo *models.HMNetInfo,
) (*deployer, error) {
conn, err := connection.NewSSHConn(
netInfo.IP.String(),
netInfo.SSH.User,
netInfo.SSH.Port,
netInfo.SSH.PrivKey,
)
if err != nil {
return nil, err
}
d := &deployer{
ctx: ctx,
type_: type_,
project: project,
processing: atomic.Bool{},
chDone: make(chan struct{}, 1),
conn: &conn,
}
d.processing.Store(false)
return d, nil
}
func (d *deployer) close() error {
return d.conn.Close()
}
func (d *deployer) setDone(err error) {
d.chDone <- struct{}{}
d.errFlag = errors.Join(d.ctx.Err(), err)
if err != nil && d.fnCancel != nil {
d.fnCancel()
}
}
func (d *deployer) clean() {
if err := os.Remove(d.archivePath); err != nil {
log.Err(err).Str("archive", d.archivePath).Msg("unable to clean local archive file")
}
if d.deploymentDir != "" {
if _, err := d.conn.Execute("rm -rf " + d.deploymentDir); err != nil {
log.Err(err).
Str("dir", d.deploymentDir).
Str("type", string(d.type_)).
Msg("unable to clean deployment dir")
}
}
}
func (d *deployer) copyUntarArchive() error {
deploymentDir := "." + d.project.Name
// TODO(rmanach): check cmd error output to check if's not an other error
if _, err := d.conn.Execute("mkdir " + deploymentDir); err != nil {
log.Error().
Str("dir", deploymentDir).
Msg("deployment dir already exists, unable to deploy now")
d.setDone(err)
return err
}
d.deploymentDir = deploymentDir
archiveName := filepath.Base(d.archivePath)
archiveDestPath := filepath.Join(deploymentDir, archiveName)
if err := d.conn.CopyFile(d.archivePath, archiveDestPath); err != nil {
d.setDone(err)
return err
}
if _, err := d.conn.Execute(fmt.Sprintf("cd %s && tar xzvf %s", deploymentDir, archiveName)); err != nil {
d.setDone(err)
return err
}
return nil
}
// SetCancellationFunc sets a context cancellation function for the deployer.
//
// If two deployers are related on the same context, one failed and you want
// to stop the execution of the others. Then, associate a cancel function
// for the deployer and the cancel func will be fired if error occurred
// during a deployment step. Stopping all the deployers.
func (d *deployer) SetCancellationFunc(fnCancel context.CancelFunc) {
d.fnCancel = fnCancel
}
func (d *deployer) Type() DeployerType {
return d.type_
}
func (d *deployer) Error() error {
return d.errFlag
}
func (d *deployer) Clear() error {
log.Debug().Str("type", string(d.type_)).Msg("clearing deployment...")
d.clean()
if err := d.close(); err != nil {
log.Err(err).Msg("unable to close conn")
}
log.Debug().Str("type", string(d.type_)).Msg("clear deployment done")
return nil
}
// Done returns a channel providing the shutdown or the termination
// of the deployer.
//
// If the context is done, it will wait until all the current actions are done
// for a graceful shutdown. It has a graceful timeout (see: `GracefulTimeout`).
//
// If the deployer is done, succeed or failed, it simply returns.
func (d *deployer) Done() <-chan struct{} {
chDone := make(chan struct{})
go func() {
defer func() {
close(chDone)
}()
for {
select {
case <-d.ctx.Done():
log.Debug().Str("deployer", string(d.type_)).Msg("context done catch")
timeout := time.NewTicker(GracefulTimeout)
tick := time.NewTicker(time.Second)
defer tick.Stop()
defer timeout.Stop()
for {
select {
case <-timeout.C:
log.Error().
Str("deployer", string(d.type_)).
Msg("timeout while waiting for graceful shutdown")
chDone <- struct{}{}
return
case <-tick.C:
if !d.processing.Load() {
chDone <- struct{}{}
return
}
tick.Reset(1 * time.Second)
}
}
case <-d.chDone:
log.Debug().Str("deployer", string(d.type_)).Msg("terminated")
chDone <- struct{}{}
return
}
}
}()
return chDone
}