229 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package deployers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sync/atomic"
 | |
| 	"time"
 | |
| 
 | |
| 	"gitea.thegux.fr/hmdeploy/connection"
 | |
| 	"gitea.thegux.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
 | |
| }
 | 
