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 }